From 4cd70f3b73fee98a7dfa0ff5b8f5f3cbeee03edd Mon Sep 17 00:00:00 2001
From: NarayanBavisetti
Date: Mon, 4 Dec 2023 12:11:36 +0530
Subject: [PATCH] Merge branch 'develop' of github.com:makeplane/plane into
develop
---
.deepsource.toml | 23 +
.env.example | 10 +-
.github/workflows/build-branch.yml | 227 ++
.github/workflows/build-test-pull-request.yml | 6 +-
.github/workflows/create-sync-pr.yml | 2 +
.gitignore | 7 +
CODE_OF_CONDUCT.md | 2 +-
CONTRIBUTING.md | 32 +-
Dockerfile | 2 -
ENV_SETUP.md | 145 +
README.md | 36 +-
apiserver/.env.example | 27 +-
apiserver/Dockerfile.api | 2 +-
apiserver/Dockerfile.dev | 52 +
apiserver/bin/takeoff | 27 +-
apiserver/bin/user_script.py | 28 -
apiserver/gunicorn.config.py | 2 +-
apiserver/package.json | 4 +
apiserver/plane/api/apps.py | 2 +-
apiserver/plane/api/middleware/__init__.py | 0
.../api/middleware/api_authentication.py | 47 +
apiserver/plane/api/permissions/__init__.py | 2 -
apiserver/plane/api/rate_limit.py | 41 +
apiserver/plane/api/serializers/__init__.py | 94 +-
apiserver/plane/api/serializers/api_token.py | 14 -
apiserver/plane/api/serializers/base.py | 100 +
apiserver/plane/api/serializers/cycle.py | 62 +-
apiserver/plane/api/serializers/inbox.py | 59 +-
apiserver/plane/api/serializers/issue.py | 520 +--
apiserver/plane/api/serializers/module.py | 109 +-
apiserver/plane/api/serializers/project.py | 189 +-
apiserver/plane/api/serializers/state.py | 14 +-
apiserver/plane/api/serializers/user.py | 75 +-
apiserver/plane/api/serializers/workspace.py | 122 +-
apiserver/plane/api/urls/__init__.py | 15 +
apiserver/plane/api/urls/cycle.py | 35 +
apiserver/plane/api/urls/inbox.py | 17 +
apiserver/plane/api/urls/issue.py | 62 +
apiserver/plane/api/urls/module.py | 26 +
apiserver/plane/api/urls/project.py | 16 +
apiserver/plane/api/urls/state.py | 16 +
apiserver/plane/api/views/__init__.py | 177 +-
apiserver/plane/api/views/analytic.py | 297 --
apiserver/plane/api/views/api_token.py | 70 -
apiserver/plane/api/views/asset.py | 125 -
apiserver/plane/api/views/auth_extended.py | 159 -
apiserver/plane/api/views/authentication.py | 458 ---
apiserver/plane/api/views/base.py | 184 +-
apiserver/plane/api/views/config.py | 40 -
apiserver/plane/api/views/cycle.py | 1135 +++----
apiserver/plane/api/views/estimate.py | 253 --
apiserver/plane/api/views/exporter.py | 100 -
apiserver/plane/api/views/external.py | 118 -
apiserver/plane/api/views/importer.py | 602 ----
apiserver/plane/api/views/inbox.py | 840 ++---
apiserver/plane/api/views/integration/base.py | 229 --
.../plane/api/views/integration/github.py | 231 --
.../plane/api/views/integration/slack.py | 73 -
apiserver/plane/api/views/issue.py | 2951 +++-------------
apiserver/plane/api/views/module.py | 714 ++--
apiserver/plane/api/views/notification.py | 363 --
apiserver/plane/api/views/oauth.py | 314 --
apiserver/plane/api/views/page.py | 321 --
apiserver/plane/api/views/project.py | 1102 +-----
apiserver/plane/api/views/state.py | 124 +-
apiserver/plane/api/views/user.py | 158 -
apiserver/plane/api/views/view.py | 350 --
apiserver/plane/api/views/workspace.py | 1533 ---------
apiserver/plane/app/__init__.py | 0
apiserver/plane/app/apps.py | 5 +
apiserver/plane/app/middleware/__init__.py | 0
.../app/middleware/api_authentication.py | 47 +
apiserver/plane/app/permissions/__init__.py | 17 +
.../plane/{api => app}/permissions/project.py | 23 +-
.../{api => app}/permissions/workspace.py | 37 +-
apiserver/plane/app/serializers/__init__.py | 104 +
.../{api => app}/serializers/analytic.py | 4 +-
apiserver/plane/app/serializers/api.py | 31 +
.../plane/{api => app}/serializers/asset.py | 0
apiserver/plane/app/serializers/base.py | 58 +
apiserver/plane/app/serializers/cycle.py | 107 +
.../{api => app}/serializers/estimate.py | 2 +-
.../{api => app}/serializers/exporter.py | 0
.../{api => app}/serializers/importer.py | 0
apiserver/plane/app/serializers/inbox.py | 57 +
.../serializers/integration/__init__.py | 2 +-
.../serializers/integration/base.py | 2 +-
.../serializers/integration/github.py | 2 +-
.../serializers/integration/slack.py | 2 +-
apiserver/plane/app/serializers/issue.py | 616 ++++
apiserver/plane/app/serializers/module.py | 198 ++
.../{api => app}/serializers/notification.py | 0
.../plane/{api => app}/serializers/page.py | 69 +-
apiserver/plane/app/serializers/project.py | 217 ++
apiserver/plane/app/serializers/state.py | 28 +
apiserver/plane/app/serializers/user.py | 195 ++
.../plane/{api => app}/serializers/view.py | 4 +-
apiserver/plane/app/serializers/webhook.py | 106 +
apiserver/plane/app/serializers/workspace.py | 153 +
apiserver/plane/app/urls/__init__.py | 48 +
apiserver/plane/app/urls/analytic.py | 46 +
apiserver/plane/app/urls/api.py | 17 +
apiserver/plane/app/urls/asset.py | 41 +
apiserver/plane/app/urls/authentication.py | 55 +
apiserver/plane/app/urls/config.py | 12 +
apiserver/plane/app/urls/cycle.py | 87 +
apiserver/plane/app/urls/estimate.py | 37 +
apiserver/plane/app/urls/external.py | 25 +
apiserver/plane/app/urls/importer.py | 37 +
apiserver/plane/app/urls/inbox.py | 53 +
apiserver/plane/app/urls/integration.py | 150 +
apiserver/plane/app/urls/issue.py | 315 ++
apiserver/plane/app/urls/module.py | 104 +
apiserver/plane/app/urls/notification.py | 66 +
apiserver/plane/app/urls/page.py | 133 +
apiserver/plane/app/urls/project.py | 172 +
apiserver/plane/app/urls/search.py | 21 +
apiserver/plane/app/urls/state.py | 38 +
apiserver/plane/app/urls/user.py | 99 +
apiserver/plane/app/urls/views.py | 85 +
apiserver/plane/app/urls/webhook.py | 31 +
apiserver/plane/app/urls/workspace.py | 197 ++
.../{api/urls.py => app/urls_deprecated.py} | 124 +-
apiserver/plane/app/views/__init__.py | 169 +
apiserver/plane/app/views/analytic.py | 383 +++
apiserver/plane/app/views/api.py | 78 +
apiserver/plane/app/views/asset.py | 78 +
apiserver/plane/app/views/auth_extended.py | 383 +++
apiserver/plane/app/views/authentication.py | 421 +++
apiserver/plane/app/views/base.py | 241 ++
apiserver/plane/app/views/config.py | 107 +
apiserver/plane/app/views/cycle.py | 808 +++++
apiserver/plane/app/views/estimate.py | 177 +
apiserver/plane/app/views/exporter.py | 80 +
apiserver/plane/app/views/external.py | 107 +
apiserver/plane/app/views/importer.py | 525 +++
apiserver/plane/app/views/inbox.py | 358 ++
.../views/integration/__init__.py | 0
apiserver/plane/app/views/integration/base.py | 171 +
.../plane/app/views/integration/github.py | 200 ++
.../plane/app/views/integration/slack.py | 79 +
apiserver/plane/app/views/issue.py | 1629 +++++++++
apiserver/plane/app/views/module.py | 524 +++
apiserver/plane/app/views/notification.py | 279 ++
apiserver/plane/app/views/oauth.py | 452 +++
apiserver/plane/app/views/page.py | 362 ++
apiserver/plane/app/views/project.py | 1050 ++++++
apiserver/plane/{api => app}/views/search.py | 198 +-
apiserver/plane/app/views/state.py | 92 +
apiserver/plane/app/views/user.py | 157 +
apiserver/plane/app/views/view.py | 249 ++
apiserver/plane/app/views/webhook.py | 132 +
apiserver/plane/app/views/workspace.py | 1329 ++++++++
.../plane/bgtasks/analytic_plot_export.py | 534 ++-
.../plane/bgtasks/email_verification_task.py | 46 -
.../plane/bgtasks/event_tracking_task.py | 30 +
apiserver/plane/bgtasks/export_task.py | 7 +-
.../plane/bgtasks/exporter_expired_task.py | 6 +-
apiserver/plane/bgtasks/file_asset_task.py | 29 +
.../plane/bgtasks/forgot_password_task.py | 81 +-
apiserver/plane/bgtasks/importer_task.py | 36 +-
.../plane/bgtasks/issue_activites_task.py | 1497 +++++----
.../plane/bgtasks/issue_automation_task.py | 22 +-
.../plane/bgtasks/magic_link_code_task.py | 75 +-
apiserver/plane/bgtasks/notification_task.py | 419 +++
.../plane/bgtasks/project_invitation_task.py | 62 +-
apiserver/plane/bgtasks/user_count_task.py | 49 +
apiserver/plane/bgtasks/webhook_task.py | 222 ++
.../bgtasks/workspace_invitation_task.py | 92 +-
apiserver/plane/celery.py | 6 +-
.../db/management/commands/create_bucket.py | 71 +
.../db/migrations/0018_auto_20230130_0119.py | 6 +-
..._alter_analyticview_created_by_and_more.py | 984 ++++++
...escription_apitoken_expired_at_and_more.py | 131 +
.../db/migrations/0048_auto_20231116_0713.py | 54 +
.../db/migrations/0049_auto_20231116_0713.py | 72 +
..._case_alter_workspace_organization_size.py | 39 +
apiserver/plane/db/models/__init__.py | 10 +-
apiserver/plane/db/models/api.py | 80 +
apiserver/plane/db/models/api_token.py | 41 -
apiserver/plane/db/models/asset.py | 1 +
apiserver/plane/db/models/exporter.py | 2 +-
.../plane/db/models/integration/__init__.py | 2 +-
.../plane/db/models/integration/github.py | 1 -
apiserver/plane/db/models/issue.py | 87 +-
apiserver/plane/db/models/module.py | 6 +-
apiserver/plane/db/models/page.py | 48 +
apiserver/plane/db/models/project.py | 4 +-
apiserver/plane/db/models/user.py | 1 +
apiserver/plane/db/models/webhook.py | 89 +
apiserver/plane/db/models/workspace.py | 25 +-
apiserver/plane/license/__init__.py | 0
apiserver/plane/license/api/__init__.py | 0
.../plane/license/api/permissions/__init__.py | 1 +
.../plane/license/api/permissions/instance.py | 19 +
.../plane/license/api/serializers/__init__.py | 1 +
.../plane/license/api/serializers/instance.py | 49 +
apiserver/plane/license/api/views/__init__.py | 9 +
apiserver/plane/license/api/views/instance.py | 495 +++
apiserver/plane/license/apps.py | 5 +
.../plane/license/management/__init__.py | 0
.../license/management/commands/__init__.py | 0
.../management/commands/configure_instance.py | 121 +
.../management/commands/register_instance.py | 88 +
.../plane/license/migrations/0001_initial.py | 86 +
.../plane/license/migrations/__init__.py | 0
apiserver/plane/license/models/__init__.py | 1 +
apiserver/plane/license/models/instance.py | 70 +
apiserver/plane/license/urls.py | 54 +
apiserver/plane/license/utils/__init__.py | 0
apiserver/plane/license/utils/encryption.py | 22 +
.../plane/license/utils/instance_value.py | 63 +
.../plane/middleware/api_log_middleware.py | 40 +
apiserver/plane/middleware/user_middleware.py | 33 -
apiserver/plane/settings/common.py | 238 +-
apiserver/plane/settings/local.py | 118 +-
apiserver/plane/settings/production.py | 267 +-
apiserver/plane/settings/redis.py | 10 +-
apiserver/plane/settings/selfhosted.py | 129 -
apiserver/plane/settings/staging.py | 224 --
apiserver/plane/settings/test.py | 44 +-
apiserver/plane/space/__init__.py | 0
apiserver/plane/space/apps.py | 5 +
apiserver/plane/space/serializer/__init__.py | 5 +
apiserver/plane/space/serializer/base.py | 58 +
apiserver/plane/space/serializer/cycle.py | 18 +
apiserver/plane/space/serializer/inbox.py | 47 +
apiserver/plane/space/serializer/issue.py | 506 +++
apiserver/plane/space/serializer/module.py | 18 +
apiserver/plane/space/serializer/project.py | 20 +
apiserver/plane/space/serializer/state.py | 28 +
apiserver/plane/space/serializer/user.py | 22 +
apiserver/plane/space/serializer/workspace.py | 15 +
apiserver/plane/space/urls/__init__.py | 10 +
apiserver/plane/space/urls/inbox.py | 49 +
apiserver/plane/space/urls/issue.py | 76 +
apiserver/plane/space/urls/project.py | 20 +
apiserver/plane/space/views/__init__.py | 15 +
apiserver/plane/space/views/base.py | 212 ++
apiserver/plane/space/views/inbox.py | 282 ++
apiserver/plane/space/views/issue.py | 656 ++++
apiserver/plane/space/views/project.py | 61 +
apiserver/plane/tests/__init__.py | 2 +-
apiserver/plane/tests/api/base.py | 2 +-
apiserver/plane/urls.py | 8 +-
apiserver/plane/utils/analytics_plot.py | 96 +-
apiserver/plane/utils/grouper.py | 2 +-
apiserver/plane/utils/imports.py | 2 +-
apiserver/plane/utils/integrations/slack.py | 20 +
apiserver/plane/utils/ip_address.py | 2 +-
apiserver/plane/utils/issue_filters.py | 219 +-
apiserver/plane/utils/markdown.py | 2 +-
apiserver/plane/utils/paginator.py | 28 +-
apiserver/requirements/base.txt | 18 +-
apiserver/requirements/production.txt | 2 -
apiserver/runtime.txt | 2 +-
.../emails/auth/email_verification.html | 11 -
.../emails/auth/forgot_password.html | 1686 +++++++++-
.../templates/emails/auth/magic_signin.html | 1847 ++++++++--
.../invitations/project_invitation.html | 2 +-
.../invitations/workspace_invitation.html | 1991 +++++++++--
deploy/coolify/README.md | 8 +
deploy/coolify/coolify-docker-compose.yml | 235 ++
deploy/heroku/Dockerfile | 4 -
deploy/kubernetes/README.md | 8 +
deploy/selfhost/README.md | 309 ++
deploy/selfhost/docker-compose.yml | 162 +
deploy/selfhost/images/download.png | Bin 0 -> 22985 bytes
deploy/selfhost/images/migrate-error.png | Bin 0 -> 14139 bytes
deploy/selfhost/images/restart.png | Bin 0 -> 15664 bytes
deploy/selfhost/images/started.png | Bin 0 -> 21489 bytes
deploy/selfhost/images/stopped.png | Bin 0 -> 16638 bytes
deploy/selfhost/images/upgrade.png | Bin 0 -> 55240 bytes
deploy/selfhost/install-private.sh | 125 +
deploy/selfhost/install.sh | 124 +
deploy/selfhost/migration-0.13-0.14.sh | 118 +
deploy/selfhost/variables.env | 71 +
docker-compose-hub.yml | 234 --
docker-compose-local.yml | 167 +
docker-compose.yml | 9 -
package.json | 11 +-
packages/editor/core/Readme.md | 116 +
packages/editor/core/package.json | 82 +
packages/editor/core/postcss.config.js | 9 +
packages/editor/core/src/index.ts | 23 +
.../editor/core/src/lib/editor-commands.ts | 151 +
packages/editor/core/src/lib/utils.ts | 50 +
packages/editor/core/src/styles/editor.css | 231 ++
.../editor/core/src/styles/github-dark.css | 85 +
packages/editor/core/src/styles/tailwind.css | 3 +
.../src/ui/components/editor-container.tsx | 24 +
.../core/src/ui/components/editor-content.tsx | 23 +
.../core/src/ui/extensions/code/index.tsx | 29 +
.../ui/extensions/custom-list-keymap/index.ts | 1 +
.../list-helpers/find-list-item-pos.ts | 33 +
.../list-helpers/get-next-list-depth.ts | 20 +
.../list-helpers/handle-backspace.ts | 78 +
.../list-helpers/handle-delete.ts | 34 +
.../list-helpers/has-list-before.ts | 19 +
.../list-helpers/has-list-item-after.ts | 20 +
.../list-helpers/has-list-item-before.ts | 20 +
.../custom-list-keymap/list-helpers/index.ts | 9 +
.../list-helpers/next-list-is-deeper.ts | 19 +
.../list-helpers/next-list-is-higher.ts | 19 +
.../custom-list-keymap/list-keymap.ts | 94 +
.../src/ui/extensions/horizontal-rule.tsx | 116 +
.../src/ui/extensions/image}/image-resize.tsx | 10 +-
.../core/src/ui/extensions/image/index.tsx | 146 +
.../ui/extensions/image/read-only-image.tsx | 17 +
.../editor/core/src/ui/extensions/index.tsx | 113 +
.../editor/core/src/ui/extensions/keymap.tsx | 54 +
.../ui/extensions/table/table-cell/index.ts | 1 +
.../extensions/table/table-cell/table-cell.ts | 58 +
.../ui/extensions/table/table-header/index.ts | 1 +
.../table/table-header/table-header.ts | 57 +
.../ui/extensions/table/table-row/index.ts | 1 +
.../extensions/table/table-row/table-row.ts | 31 +
.../src/ui/extensions/table/table/icons.ts | 51 +
.../src/ui/extensions/table/table/index.ts | 1 +
.../extensions/table/table/table-controls.ts | 122 +
.../ui/extensions/table/table/table-view.tsx | 536 +++
.../src/ui/extensions/table/table/table.ts | 312 ++
.../table/table/utilities/create-cell.ts | 12 +
.../table/table/utilities/create-table.ts | 45 +
.../delete-table-when-all-cells-selected.ts | 42 +
.../table/utilities/get-table-node-types.ts | 21 +
.../table/utilities/is-cell-selection.ts | 5 +
.../editor/core/src/ui/hooks/useEditor.tsx | 103 +
.../src/ui/hooks/useInitializedContent.tsx | 19 +
.../core/src/ui/hooks/useReadOnlyEditor.tsx | 72 +
packages/editor/core/src/ui/index.tsx | 105 +
.../core/src/ui/mentions/MentionList.tsx | 119 +
.../editor/core/src/ui/mentions/custom.tsx | 58 +
.../editor/core/src/ui/mentions/index.tsx | 19 +
.../core/src/ui/mentions/mentionNodeView.tsx | 41 +
.../editor/core/src/ui/mentions/suggestion.ts | 63 +
.../core/src/ui/menus/menu-items/index.tsx | 145 +
.../core/src/ui}/plugins/delete-image.tsx | 36 +-
.../core/src/ui/plugins/upload-image.tsx | 188 ++
.../editor/core/src/ui}/props.tsx | 38 +-
.../core/src/ui/read-only/extensions.tsx | 102 +
.../editor/core/src/ui/read-only/props.tsx | 7 +
packages/editor/core/tailwind.config.js | 6 +
packages/editor/core/tsconfig.json | 5 +
packages/editor/core/tsup.config.ts | 11 +
packages/editor/document-editor/Readme.md | 1 +
packages/editor/document-editor/package.json | 59 +
.../editor/document-editor/postcss.config.js | 9 +
packages/editor/document-editor/src/index.ts | 6 +
.../src/ui/components/alert-label.tsx | 21 +
.../src/ui/components/content-browser.tsx | 40 +
.../src/ui/components/editor-header.tsx | 90 +
.../src/ui/components/heading-component.tsx | 31 +
.../src/ui/components/index.ts | 9 +
.../src/ui/components/info-popover.tsx | 79 +
.../src/ui/components/page-renderer.tsx | 35 +
.../src/ui/components/summary-popover.tsx | 57 +
.../src/ui/components/summary-side-bar.tsx | 25 +
.../ui/components/vertical-dropdown-menu.tsx | 61 +
.../src/ui/extensions/index.tsx | 28 +
.../src/ui/hooks/use-editor-markings.tsx | 36 +
.../editor/document-editor/src/ui/index.tsx | 158 +
.../src/ui/menu/fixed-menu.tsx | 177 +
.../document-editor/src/ui/menu}/icon.tsx | 4 +-
.../document-editor/src/ui/menu/index.tsx | 1 +
.../document-editor/src/ui/readonly/index.tsx | 122 +
.../document-editor/src}/ui/tooltip.tsx | 14 +-
.../src/ui/types/editor-types.ts | 7 +
.../src/ui/types/menu-actions.d.ts | 13 +
.../src/ui/utils/editor-summary-utils.ts | 34 +
.../src/ui/utils/menu-actions.ts | 12 +
.../src/ui/utils/menu-options.ts | 94 +
.../editor/document-editor/tailwind.config.js | 6 +
packages/editor/document-editor/tsconfig.json | 5 +
.../editor/document-editor/tsup.config.ts | 11 +
packages/editor/extensions/Readme.md | 97 +
packages/editor/extensions/package.json | 60 +
packages/editor/extensions/postcss.config.js | 9 +
.../extensions/src/extensions/drag-drop.tsx | 256 ++
.../src/extensions/slash-commands.tsx | 116 +-
packages/editor/extensions/src/index.ts | 2 +
packages/editor/extensions/tailwind.config.js | 6 +
packages/editor/extensions/tsconfig.json | 5 +
packages/editor/extensions/tsup.config.ts | 11 +
packages/editor/lite-text-editor/Readme.md | 97 +
packages/editor/lite-text-editor/package.json | 55 +
.../editor/lite-text-editor/postcss.config.js | 9 +
packages/editor/lite-text-editor/src/index.ts | 6 +
.../src/ui/extensions/enter-key-extension.tsx | 25 +
.../src/ui/extensions/index.tsx | 5 +
.../editor/lite-text-editor/src/ui/index.tsx | 134 +
.../src/ui/menus/fixed-menu/icon.tsx | 14 +
.../src/ui/menus/fixed-menu/index.tsx | 254 ++
.../src/ui/read-only/index.tsx | 69 +
.../lite-text-editor/src/ui/tooltip.tsx | 84 +
.../lite-text-editor/tailwind.config.js | 6 +
.../editor/lite-text-editor/tsconfig.json | 5 +
.../editor/lite-text-editor/tsup.config.ts | 11 +
packages/editor/rich-text-editor/Readme.md | 103 +
packages/editor/rich-text-editor/package.json | 58 +
.../editor/rich-text-editor/postcss.config.js | 9 +
packages/editor/rich-text-editor/src/index.ts | 7 +
.../src/ui/extensions/index.tsx | 30 +
.../editor/rich-text-editor/src/ui/index.tsx | 115 +
.../src/ui/menus/bubble-menu/index.tsx | 145 +
.../ui/menus}/bubble-menu/link-selector.tsx | 30 +-
.../ui/menus}/bubble-menu/node-selector.tsx | 96 +-
.../src/ui/read-only/index.tsx | 70 +
.../rich-text-editor/tailwind.config.js | 6 +
.../editor/rich-text-editor/tsconfig.json | 5 +
.../editor/rich-text-editor/tsup.config.ts | 11 +
packages/editor/types/Readme.md | 97 +
packages/editor/types/package.json | 50 +
packages/editor/types/postcss.config.js | 9 +
packages/editor/types/src/index.ts | 7 +
.../editor/types/src/types/delete-image.ts | 1 +
.../types/src/types/mention-suggestion.ts | 10 +
.../editor/types/src/types/restore-image.ts | 1 +
.../editor/types/src/types/upload-image.ts | 1 +
packages/editor/types/tailwind.config.js | 6 +
packages/editor/types/tsconfig.json | 5 +
packages/editor/types/tsup.config.ts | 11 +
packages/eslint-config-custom/package.json | 1 +
packages/tailwind-config-custom/package.json | 10 +-
.../tailwind-config-custom/tailwind.config.js | 192 +-
packages/tsconfig/base.json | 4 +-
packages/tsconfig/react-library.json | 7 +-
packages/ui/button/index.tsx | 3 -
packages/ui/index.tsx | 17 -
packages/ui/package.json | 40 +-
packages/ui/src/avatar/avatar-group.tsx | 91 +
packages/ui/src/avatar/avatar.tsx | 175 +
packages/ui/src/avatar/index.ts | 2 +
packages/ui/src/badge/badge.tsx | 64 +
packages/ui/src/badge/helper.tsx | 145 +
packages/ui/src/badge/index.ts | 1 +
packages/ui/src/breadcrumbs/breadcrumbs.tsx | 81 +
packages/ui/src/breadcrumbs/index.ts | 1 +
packages/ui/src/button/button.tsx | 66 +
packages/ui/src/button/helper.tsx | 123 +
packages/ui/src/button/index.ts | 2 +
.../ui/src/button}/toggle-switch.tsx | 12 +-
packages/ui/src/dropdowns/custom-menu.tsx | 155 +
.../ui/src/dropdowns/custom-search-select.tsx | 204 ++
packages/ui/src/dropdowns/custom-select.tsx | 135 +
packages/ui/src/dropdowns/helper.tsx | 70 +
.../ui => packages/ui/src}/dropdowns/index.ts | 4 +-
packages/ui/src/form-fields/index.ts | 3 +
.../ui/src/form-fields/input-color-picker.tsx | 117 +
packages/ui/src/form-fields/input.tsx | 49 +
packages/ui/src/form-fields/textarea.tsx | 69 +
packages/ui/src/icons/admin-profile-icon.tsx | 33 +
packages/ui/src/icons/archive-icon.tsx | 35 +
packages/ui/src/icons/blocked-icon.tsx | 31 +
packages/ui/src/icons/blocker-icon.tsx | 31 +
packages/ui/src/icons/calendar-after-icon.tsx | 33 +
.../ui/src/icons/calendar-before-icon.tsx | 39 +
packages/ui/src/icons/center-panel-icon.tsx | 33 +
packages/ui/src/icons/copy-icon.tsx | 28 +
packages/ui/src/icons/create-icon.tsx | 25 +
.../src/icons/cycle/circle-dot-full-icon.tsx | 25 +
packages/ui/src/icons/cycle/contrast-icon.tsx | 29 +
.../ui/src/icons/cycle/cycle-group-icon.tsx | 33 +
.../ui/src/icons/cycle/double-circle-icon.tsx | 20 +
packages/ui/src/icons/cycle/helper.tsx | 18 +
packages/ui/src/icons/cycle/index.ts | 5 +
packages/ui/src/icons/dice-icon.tsx | 47 +
packages/ui/src/icons/discord-icon.tsx | 31 +
packages/ui/src/icons/external-link-icon.tsx | 25 +
.../ui/src/icons/full-screen-panel-icon.tsx | 29 +
packages/ui/src/icons/github-icon.tsx | 33 +
packages/ui/src/icons/index.ts | 27 +
packages/ui/src/icons/layer-stack.tsx | 33 +
packages/ui/src/icons/layers-icon.tsx | 45 +
packages/ui/src/icons/module/backlog.tsx | 39 +
packages/ui/src/icons/module/cancelled.tsx | 30 +
packages/ui/src/icons/module/completed.tsx | 23 +
packages/ui/src/icons/module/in-progress.tsx | 43 +
packages/ui/src/icons/module/index.ts | 7 +
.../src/icons/module/module-status-icon.tsx | 67 +
packages/ui/src/icons/module/paused.tsx | 30 +
packages/ui/src/icons/module/planned.tsx | 23 +
packages/ui/src/icons/photo-filter-icon.tsx | 35 +
packages/ui/src/icons/priority-icon.tsx | 57 +
packages/ui/src/icons/related-icon.tsx | 33 +
packages/ui/src/icons/running-icon.tsx | 18 +
packages/ui/src/icons/side-panel-icon.tsx | 29 +
.../ui/src/icons/state/backlog-group-icon.tsx | 39 +
.../src/icons/state/cancelled-group-icon.tsx | 32 +
.../src/icons/state/completed-group-icon.tsx | 25 +
packages/ui/src/icons/state/helper.tsx | 24 +
packages/ui/src/icons/state/index.ts | 6 +
.../ui/src/icons/state/started-group-icon.tsx | 43 +
.../ui/src/icons/state/state-group-icon.tsx | 35 +
.../src/icons/state/unstarted-group-icon.tsx | 21 +
packages/ui/src/icons/subscribe-icon.tsx | 33 +
packages/ui/src/icons/transfer-icon.tsx | 18 +
packages/ui/src/icons/type.d.ts | 11 +
packages/ui/src/icons/user-group-icon.tsx | 38 +
packages/ui/src/index.ts | 11 +
.../ui => packages/ui/src}/loader.tsx | 7 +-
.../progress/circular-progress-indicator.tsx | 102 +
packages/ui/src/progress/index.ts | 4 +
.../progress}/linear-progress-indicator.tsx | 12 +-
.../ui/src/progress}/progress-bar.tsx | 16 +-
packages/ui/src/progress/radial-progress.tsx | 45 +
.../ui/src/spinners/circular-spinner.tsx | 17 +-
packages/ui/src/spinners/index.ts | 1 +
packages/ui/src/tooltip/index.ts | 1 +
packages/ui/src/tooltip/tooltip.tsx | 71 +
packages/ui/tsconfig.json | 3 +
space/Dockerfile.dev | 11 +
.../accounts/email-password-form.tsx | 8 +-
.../accounts/email-reset-password-form.tsx | 30 +-
space/components/accounts/onboarding-form.tsx | 36 +-
space/components/accounts/sign-in.tsx | 4 +-
space/components/icons/index.ts | 1 -
.../icons/state-group/backlog-state-icon.tsx | 23 -
.../state-group/cancelled-state-icon.tsx | 74 -
.../state-group/completed-state-icon.tsx | 65 -
space/components/icons/state-group/index.ts | 6 -
.../icons/state-group/started-state-icon.tsx | 73 -
.../icons/state-group/state-group-icon.tsx | 29 -
.../state-group/unstarted-state-icon.tsx | 55 -
space/components/icons/types.d.ts | 6 -
.../issues/board-views/block-state.tsx | 4 +-
.../issues/board-views/kanban/block.tsx | 9 +-
.../issues/board-views/kanban/header.tsx | 12 +-
.../issues/board-views/list/block.tsx | 6 +-
.../issues/board-views/list/header.tsx | 14 +-
.../issues/board-views/list/index.tsx | 4 +-
.../state/filter-state-block.tsx | 17 +-
space/components/issues/navbar/index.tsx | 28 +-
.../issues/navbar/issue-board-view.tsx | 20 +-
.../components/issues/navbar/issue-filter.tsx | 9 +-
space/components/issues/navbar/issue-view.tsx | 13 -
space/components/issues/navbar/search.tsx | 13 -
.../peek-overview/comment/add-comment.tsx | 50 +-
.../comment/comment-detail-card.tsx | 38 +-
.../peek-overview/full-screen-peek-view.tsx | 13 +-
.../issues/peek-overview/header.tsx | 14 +-
.../issues/peek-overview/issue-activity.tsx | 26 +-
.../issues/peek-overview/issue-details.tsx | 13 +-
.../issues/peek-overview/issue-properties.tsx | 4 +-
.../issues/peek-overview/layout.tsx | 9 +-
space/components/tiptap/bubble-menu/index.tsx | 121 -
.../bubble-menu/utils/link-validator.tsx | 11 -
.../tiptap/extensions/image-resize.tsx | 44 -
space/components/tiptap/extensions/index.tsx | 149 -
.../tiptap/extensions/table/table-cell.ts | 32 -
.../tiptap/extensions/table/table-header.ts | 7 -
.../tiptap/extensions/table/table.ts | 9 -
.../tiptap/extensions/updated-image.tsx | 22 -
space/components/tiptap/index.tsx | 110 -
.../tiptap/plugins/upload-image.tsx | 127 -
space/components/tiptap/props.tsx | 69 -
.../table-menu/InsertBottomTableIcon.tsx | 16 -
.../tiptap/table-menu/InsertLeftTableIcon.tsx | 15 -
.../table-menu/InsertRightTableIcon.tsx | 16 -
.../tiptap/table-menu/InsertTopTableIcon.tsx | 15 -
space/components/tiptap/table-menu/index.tsx | 143 -
space/components/tiptap/utils.ts | 6 -
space/components/ui/dropdown.tsx | 13 +-
space/components/ui/toast-alert.tsx | 18 +-
space/components/ui/tooltip.tsx | 5 +-
space/components/views/login.tsx | 2 +-
space/components/views/project-details.tsx | 21 +-
space/constants/data.ts | 14 -
space/helpers/common.helper.ts | 7 +-
space/hooks/use-editor-suggestions.tsx | 13 +
space/lib/mobx/store-init.tsx | 8 -
space/next.config.js | 3 +
space/package.json | 47 +-
space/pages/_app.tsx | 1 +
space/pages/_document.tsx | 2 +-
space/services/app-config.service.ts | 11 +-
space/services/file.service.ts | 59 +-
space/store/mentions.store.ts | 43 +
space/store/root.ts | 3 +
space/store/user.ts | 1 -
space/styles/editor.css | 105 +-
space/styles/globals.css | 6 +-
space/styles/table.css | 210 ++
space/types/issue.ts | 8 +-
turbo.json | 88 +-
web/.prettierrc | 2 +-
web/Dockerfile.dev | 2 +-
.../account/deactivate-account-modal.tsx | 119 +
web/components/account/email-code-form.tsx | 215 --
.../account/email-password-form.tsx | 94 -
.../account/email-reset-password-form.tsx | 97 -
web/components/account/email-signup-form.tsx | 95 +-
.../account/github-login-button.tsx | 22 +-
web/components/account/google-login.tsx | 7 +-
web/components/account/index.ts | 5 +-
.../account/sign-in-forms/create-password.tsx | 141 +
.../account/sign-in-forms/email-form.tsx | 152 +
web/components/account/sign-in-forms/index.ts | 8 +
.../account/sign-in-forms/o-auth-options.tsx | 104 +
.../sign-in-forms/optional-set-password.tsx | 101 +
.../account/sign-in-forms/password.tsx | 210 ++
web/components/account/sign-in-forms/root.tsx | 86 +
.../sign-in-forms/set-password-link.tsx | 134 +
.../account/sign-in-forms/unique-code.tsx | 241 ++
.../create-update-analytics-modal.tsx | 161 -
.../custom-analytics/custom-analytics.tsx | 147 +-
.../custom-analytics/graph/custom-tooltip.tsx | 5 +-
.../custom-analytics/graph/index.tsx | 88 +-
.../analytics/custom-analytics/index.ts | 3 +-
.../custom-analytics/main-content.tsx | 85 +
.../analytics/custom-analytics/select-bar.tsx | 124 +-
.../{ => custom-analytics}/select/index.ts | 0
.../{ => custom-analytics}/select/project.tsx | 10 +-
.../{ => custom-analytics}/select/segment.tsx | 6 +-
.../{ => custom-analytics}/select/x-axis.tsx | 14 +-
.../{ => custom-analytics}/select/y-axis.tsx | 2 +-
.../analytics/custom-analytics/sidebar.tsx | 381 ---
.../custom-analytics/sidebar/index.ts | 3 +
.../sidebar/projects-list.tsx | 65 +
.../sidebar/sidebar-header.tsx | 107 +
.../custom-analytics/sidebar/sidebar.tsx | 200 ++
.../analytics/custom-analytics/table.tsx | 185 +-
web/components/analytics/index.ts | 1 -
web/components/analytics/project-modal.tsx | 225 --
.../analytics/project-modal/header.tsx | 37 +
.../analytics/project-modal/index.ts | 3 +
.../analytics/project-modal/main-content.tsx | 54 +
.../analytics/project-modal/modal.tsx | 70 +
.../analytics/scope-and-demand/demand.tsx | 4 +-
.../scope-and-demand/leaderboard.tsx | 7 +-
.../scope-and-demand/scope-and-demand.tsx | 34 +-
.../analytics/scope-and-demand/scope.tsx | 4 +-
.../scope-and-demand/year-wise-issues.tsx | 13 +-
.../api-token/delete-token-modal.tsx | 126 +
web/components/api-token/empty-state.tsx | 31 +
web/components/api-token/index.ts | 4 +
.../api-token/modal/create-token-modal.tsx | 133 +
web/components/api-token/modal/form.tsx | 247 ++
.../modal/generated-token-details.tsx | 61 +
web/components/api-token/modal/index.ts | 3 +
web/components/api-token/token-list-item.tsx | 58 +
.../auth-screens/not-authorized-view.tsx | 8 +-
.../auth-screens/project/join-project.tsx | 49 +-
.../auth-screens/workspace/not-a-member.tsx | 18 +-
.../automation/auto-archive-automation.tsx | 109 +-
.../automation/auto-close-automation.tsx | 212 +-
.../automation/select-month-modal.tsx | 115 +-
web/components/breadcrumbs/index.tsx | 19 +-
.../command-palette/actions/help-actions.tsx | 83 +
.../command-palette/actions/index.ts | 6 +
.../actions/issue-actions/actions-list.tsx | 166 +
.../actions/issue-actions/change-assignee.tsx | 79 +
.../actions/issue-actions/change-priority.tsx | 56 +
.../actions/issue-actions/change-state.tsx | 65 +
.../actions/issue-actions/index.ts | 4 +
.../actions/project-actions.tsx | 85 +
.../actions/search-results.tsx | 49 +
.../command-palette/actions/theme-actions.tsx | 65 +
.../actions/workspace-settings-actions.tsx | 61 +
.../change-interface-theme.tsx | 65 -
web/components/command-palette/command-k.tsx | 793 -----
.../command-palette/command-modal.tsx | 388 +++
.../command-palette/command-pallette.tsx | 220 +-
web/components/command-palette/helpers.tsx | 42 +-
web/components/command-palette/index.ts | 7 +-
.../issue/change-issue-assignee.tsx | 108 -
.../issue/change-issue-priority.tsx | 81 -
.../issue/change-issue-state.tsx | 106 -
web/components/command-palette/issue/index.ts | 3 -
.../command-palette/shortcuts-modal.tsx | 206 --
.../shortcuts-modal/commands-list.tsx | 98 +
.../command-palette/shortcuts-modal/index.ts | 2 +
.../command-palette/shortcuts-modal/modal.tsx | 81 +
web/components/{ui => common}/empty-state.tsx | 22 +-
web/components/common/index.ts | 3 +
.../common/latest-feature-block.tsx | 36 +
web/components/common/new-empty-state.tsx | 111 +
.../common/product-updates-modal.tsx | 112 +
web/components/core/activity.tsx | 296 +-
.../core/filters/date-filter-modal.tsx | 73 +-
.../core/filters/date-filter-select.tsx | 6 +-
web/components/core/filters/filters-list.tsx | 346 --
web/components/core/filters/index.ts | 3 -
.../core/filters/issues-view-filter.tsx | 406 ---
.../core/filters/workspace-filters-list.tsx | 364 --
web/components/core/image-picker-popover.tsx | 106 +-
web/components/core/index.ts | 1 -
.../core/modals/bulk-delete-issues-modal.tsx | 288 +-
.../modals/existing-issues-list-modal.tsx | 101 +-
.../core/modals/gpt-assistant-modal.tsx | 178 +-
web/components/core/modals/index.ts | 3 +-
web/components/core/modals/link-modal.tsx | 96 +-
.../core/modals/user-image-upload-modal.tsx | 199 ++
...l.tsx => workspace-image-upload-modal.tsx} | 154 +-
web/components/core/reaction-selector.tsx | 21 +-
web/components/core/sidebar/links-list.tsx | 133 +-
.../core/sidebar/progress-chart.tsx | 3 +-
.../core/sidebar/sidebar-progress-stats.tsx | 222 +-
.../core/sidebar/single-progress-stats.tsx | 9 +-
.../core/theme/color-picker-input.tsx | 59 +-
.../core/theme/custom-theme-selector.tsx | 263 +-
web/components/core/theme/theme-switch.tsx | 187 +-
web/components/core/views/all-views.tsx | 229 --
.../core/views/board-view/all-boards.tsx | 152 -
.../core/views/board-view/board-header.tsx | 231 --
web/components/core/views/board-view/index.ts | 5 -
.../board-view/inline-create-issue-form.tsx | 62 -
.../core/views/board-view/single-board.tsx | 293 --
.../core/views/board-view/single-issue.tsx | 547 ---
.../views/calendar-view/calendar-header.tsx | 153 -
.../core/views/calendar-view/calendar.tsx | 217 --
.../core/views/calendar-view/index.ts | 5 -
.../inline-create-issue-form.tsx | 102 -
.../core/views/calendar-view/single-date.tsx | 126 -
.../core/views/calendar-view/single-issue.tsx | 403 ---
.../core/views/gantt-chart-view/index.tsx | 30 -
.../inline-create-issue-form.tsx | 62 -
web/components/core/views/index.ts | 8 -
.../views/inline-issue-create-wrapper.tsx | 273 --
web/components/core/views/issues-view.tsx | 633 ----
.../core/views/list-view/all-lists.tsx | 104 -
web/components/core/views/list-view/index.ts | 4 -
.../list-view/inline-create-issue-form.tsx | 62 -
.../core/views/list-view/single-issue.tsx | 511 ---
.../core/views/list-view/single-list.tsx | 378 ---
.../assignee-column/assignee-column.tsx | 72 -
.../spreadsheet-view/assignee-column/index.ts | 2 -
.../spreadsheet-assignee-column.tsx | 62 -
.../created-on-column/created-on-column.tsx | 34 -
.../created-on-column/index.ts | 2 -
.../spreadsheet-created-on-column.tsx | 62 -
.../due-date-column/due-date-column.tsx | 38 -
.../spreadsheet-view/due-date-column/index.ts | 2 -
.../spreadsheet-due-date-column.tsx | 62 -
.../estimate-column/estimate-column.tsx | 38 -
.../spreadsheet-view/estimate-column/index.ts | 2 -
.../spreadsheet-estimate-column.tsx | 62 -
.../issue-column/issue-column.tsx | 180 -
.../spreadsheet-view/label-column/index.ts | 2 -
.../label-column/label-column.tsx | 47 -
.../label-column/spreadsheet-label-column.tsx | 62 -
.../spreadsheet-view/priority-column/index.ts | 2 -
.../priority-column/priority-column.tsx | 64 -
.../spreadsheet-priority-column.tsx | 62 -
.../views/spreadsheet-view/single-issue.tsx | 464 ---
.../spreadsheet-view/spreadsheet-view.tsx | 672 ----
.../start-date-column/index.ts | 2 -
.../spreadsheet-start-date-column.tsx | 62 -
.../start-date-column/start-date-column.tsx | 38 -
.../spreadsheet-view/state-column/index.ts | 2 -
.../state-column/spreadsheet-state-column.tsx | 62 -
.../state-column/state-column.tsx | 87 -
.../updated-on-column/index.ts | 2 -
.../spreadsheet-updated-on-column.tsx | 62 -
.../updated-on-column/updated-on-column.tsx | 34 -
.../cycles/active-cycle-details.tsx | 302 +-
web/components/cycles/active-cycle-stats.tsx | 41 +-
web/components/cycles/cycle-peek-overview.tsx | 55 +
web/components/cycles/cycles-board-card.tsx | 278 ++
web/components/cycles/cycles-board.tsx | 70 +
web/components/cycles/cycles-list-item.tsx | 269 ++
web/components/cycles/cycles-list.tsx | 81 +
.../cycles/cycles-list/all-cycles-list.tsx | 29 -
.../cycles-list/completed-cycles-list.tsx | 33 -
.../cycles/cycles-list/draft-cycles-list.tsx | 29 -
web/components/cycles/cycles-list/index.ts | 4 -
.../cycles-list/upcoming-cycles-list.tsx | 33 -
web/components/cycles/cycles-view.tsx | 336 +-
web/components/cycles/delete-cycle-modal.tsx | 185 -
web/components/cycles/delete-modal.tsx | 143 +
web/components/cycles/form.tsx | 171 +-
web/components/cycles/gantt-chart/blocks.tsx | 8 +-
.../gantt-chart/cycle-issues-layout.tsx | 64 -
.../cycles/gantt-chart/cycles-list-layout.tsx | 63 +-
web/components/cycles/gantt-chart/index.ts | 1 -
web/components/cycles/index.ts | 13 +-
web/components/cycles/modal.tsx | 167 +-
web/components/cycles/select.tsx | 134 -
web/components/cycles/sidebar.tsx | 702 ++--
web/components/cycles/single-cycle-card.tsx | 415 ---
web/components/cycles/single-cycle-list.tsx | 394 ---
.../cycles/transfer-issues-modal.tsx | 73 +-
web/components/cycles/transfer-issues.tsx | 31 +-
web/components/dnd/StrictModeDroppable.tsx | 2 +-
web/components/emoji-icon-picker/index.tsx | 30 +-
.../create-update-estimate-modal.tsx | 328 +-
.../estimates/delete-estimate-modal.tsx | 91 +-
...le-estimate.tsx => estimate-list-item.tsx} | 102 +-
web/components/estimates/estimate-select.tsx | 160 +
web/components/estimates/estimates-list.tsx | 138 +
web/components/estimates/index.ts | 5 +
web/components/estimates/index.tsx | 3 -
web/components/exporter/export-modal.tsx | 81 +-
web/components/exporter/guide.tsx | 74 +-
web/components/exporter/single-export.tsx | 21 +-
.../gantt-chart/blocks/blocks-display.tsx | 12 +-
web/components/gantt-chart/chart/bi-week.tsx | 9 +-
web/components/gantt-chart/chart/day.tsx | 9 +-
web/components/gantt-chart/chart/hours.tsx | 9 +-
web/components/gantt-chart/chart/index.tsx | 141 +-
web/components/gantt-chart/chart/month.tsx | 16 +-
web/components/gantt-chart/chart/quarter.tsx | 5 +-
web/components/gantt-chart/chart/week.tsx | 9 +-
web/components/gantt-chart/chart/year.tsx | 5 +-
web/components/gantt-chart/contexts/index.tsx | 9 +-
web/components/gantt-chart/data/index.ts | 11 +-
.../gantt-chart/helpers/draggable.tsx | 35 +-
.../gantt-chart/hooks/block-update.tsx | 41 -
web/components/gantt-chart/index.ts | 1 +
web/components/gantt-chart/root.tsx | 12 +-
.../cycle-sidebar.tsx} | 39 +-
web/components/gantt-chart/sidebar/index.ts | 4 +
.../gantt-chart/sidebar/module-sidebar.tsx | 159 +
.../sidebar/project-view-sidebar.tsx | 160 +
.../gantt-chart/sidebar/sidebar.tsx | 171 +
.../gantt-chart/views/bi-week-view.ts | 47 +-
web/components/gantt-chart/views/day-view.ts | 54 +-
web/components/gantt-chart/views/helpers.ts | 9 +-
.../gantt-chart/views/hours-view.ts | 54 +-
.../gantt-chart/views/month-view.ts | 60 +-
.../gantt-chart/views/quater-view.ts | 5 +-
web/components/gantt-chart/views/week-view.ts | 47 +-
web/components/headers/cycle-issues.tsx | 214 ++
web/components/headers/cycles.tsx | 65 +
web/components/headers/global-issues.tsx | 153 +
web/components/headers/index.ts | 22 +
web/components/headers/module-issues.tsx | 215 ++
web/components/headers/modules-list.tsx | 86 +
web/components/headers/page-details.tsx | 90 +
web/components/headers/pages.tsx | 67 +
.../headers/profile-preferences.tsx | 14 +
web/components/headers/profile-settings.tsx | 30 +
.../project-archived-issue-details.tsx | 78 +
.../headers/project-archived-issues.tsx | 140 +
.../headers/project-draft-issues.tsx | 135 +
web/components/headers/project-inbox.tsx | 62 +
.../headers/project-issue-details.tsx | 72 +
web/components/headers/project-issues.tsx | 217 ++
web/components/headers/project-settings.tsx | 51 +
.../headers/project-view-issues.tsx | 176 +
web/components/headers/project-views.tsx | 78 +
web/components/headers/projects.tsx | 53 +
web/components/headers/user-profile.tsx | 14 +
.../headers/workspace-analytics.tsx | 37 +
.../headers/workspace-dashboard.tsx | 62 +
web/components/headers/workspace-settings.tsx | 39 +
web/components/icons/alarm-clock-icon.tsx | 1 +
web/components/icons/audio-file-icon.tsx | 10 +-
web/components/icons/cmd-icon.tsx | 7 +-
web/components/icons/command-icon.tsx | 5 +-
web/components/icons/completed-cycle-icon.tsx | 7 +-
web/components/icons/css-file-icon.tsx | 7 +-
web/components/icons/csv-file-icon.tsx | 7 +-
web/components/icons/current-cycle-icon.tsx | 7 +-
web/components/icons/default-file-icon.tsx | 7 +-
web/components/icons/doc-file-icon.tsx | 7 +-
web/components/icons/document-icon.tsx | 7 +-
web/components/icons/external-link-icon.tsx | 7 +-
web/components/icons/figma-file-icon.tsx | 7 +-
web/components/icons/html-file-icon.tsx | 7 +-
web/components/icons/img-file-icon.tsx | 7 +-
web/components/icons/index.ts | 2 +-
web/components/icons/jpg-file-icon.tsx | 7 +-
web/components/icons/js-file-icon.tsx | 7 +-
web/components/icons/module-icon.tsx | 37 +-
web/components/icons/module/cancelled.tsx | 6 +-
web/components/icons/module/completed.tsx | 6 +-
web/components/icons/module/in-progress.tsx | 11 +-
.../icons/module/module-status-icon.tsx | 19 +-
web/components/icons/pdf-file-icon.tsx | 7 +-
web/components/icons/pencil-scribble-icon.tsx | 15 +-
web/components/icons/png-file-icon.tsx | 7 +-
.../icons/question-mark-circle-icon.tsx | 6 +-
web/components/icons/sheet-file-icon.tsx | 7 +-
web/components/icons/single-comment-icon.tsx | 7 +-
web/components/icons/svg-file-icon.tsx | 7 +-
web/components/icons/tag-icon.tsx | 7 +-
.../icons/triangle-exclamation-icon.tsx | 6 +-
web/components/icons/txt-file-icon.tsx | 7 +-
web/components/icons/types.d.ts | 5 +
web/components/icons/upcoming-cycle-icon.tsx | 7 +-
web/components/icons/user-group-icon.tsx | 7 +-
web/components/icons/video-file-icon.tsx | 7 +-
...-action-headers.tsx => actions-header.tsx} | 231 +-
web/components/inbox/filters-dropdown.tsx | 49 +-
web/components/inbox/filters-list.tsx | 83 +-
web/components/inbox/inbox-issue-card.tsx | 123 -
web/components/inbox/index.ts | 13 +-
...-issue-activity.tsx => issue-activity.tsx} | 74 +-
web/components/inbox/issue-card.tsx | 95 +
web/components/inbox/issues-list-sidebar.tsx | 33 +-
...nbox-main-content.tsx => main-content.tsx} | 185 +-
.../inbox/{ => modals}/accept-issue-modal.tsx | 33 +-
.../inbox/modals/create-issue-modal.tsx | 318 ++
.../{ => modals}/decline-issue-modal.tsx | 36 +-
.../inbox/{ => modals}/delete-issue-modal.tsx | 77 +-
web/components/inbox/modals/index.ts | 5 +
.../inbox/{ => modals}/select-duplicate.tsx | 63 +-
web/components/instance/ai-form.tsx | 150 +
web/components/instance/email-form.tsx | 231 ++
web/components/instance/general-form.tsx | 127 +
.../instance/github-config-form.tsx | 182 +
.../instance/google-config-form.tsx | 131 +
web/components/instance/help-section.tsx | 131 +
web/components/instance/image-config-form.tsx | 116 +
web/components/instance/index.ts | 14 +
.../instance/instance-admin-restriction.tsx | 77 +
web/components/instance/not-ready-view.tsx | 34 +
web/components/instance/setup-done-view.tsx | 66 +
.../instance/setup-form/email-code-form.tsx | 160 +
.../instance/setup-form/email-form.tsx | 103 +
web/components/instance/setup-form/index.ts | 3 +
.../instance/setup-form/password-form.tsx | 126 +
web/components/instance/setup-form/root.tsx | 68 +
web/components/instance/setup-view.tsx | 38 +
web/components/instance/sidebar-dropdown.tsx | 157 +
web/components/instance/sidebar-menu.tsx | 93 +
.../integration/delete-import-modal.tsx | 52 +-
web/components/integration/github/auth.tsx | 26 +-
.../integration/github/import-configure.tsx | 12 +-
.../integration/github/import-confirm.tsx | 13 +-
.../integration/github/import-data.tsx | 46 +-
.../integration/github/import-users.tsx | 21 +-
.../integration/github/repo-details.tsx | 29 +-
web/components/integration/github/root.tsx | 78 +-
.../integration/github/select-repository.tsx | 33 +-
.../integration/github/single-user-select.tsx | 22 +-
web/components/integration/guide.tsx | 58 +-
.../integration/jira/confirm-import.tsx | 4 +-
.../integration/jira/give-details.tsx | 147 +-
.../integration/jira/import-users.tsx | 73 +-
.../integration/jira/jira-project-detail.tsx | 11 +-
web/components/integration/jira/root.tsx | 87 +-
web/components/integration/single-import.tsx | 14 +-
.../integration/single-integration-card.tsx | 95 +-
.../integration/slack/select-channel.tsx | 44 +-
web/components/issues/activity.tsx | 35 +-
.../issues/attachment/attachment-upload.tsx | 44 +-
.../issues/attachment/attachments.tsx | 77 +-
.../attachment/delete-attachment-modal.tsx | 52 +-
web/components/issues/comment/add-comment.tsx | 125 +-
.../issues/comment/comment-card.tsx | 87 +-
.../issues/comment/comment-reaction.tsx | 51 +-
.../issues/confirm-issue-discard.tsx | 29 +-
.../issues/delete-archived-issue-modal.tsx | 137 +
.../issues/delete-draft-issue-modal.tsx | 61 +-
web/components/issues/delete-issue-modal.tsx | 195 +-
web/components/issues/description-form.tsx | 141 +-
web/components/issues/draft-issue-form.tsx | 251 +-
web/components/issues/draft-issue-modal.tsx | 202 +-
web/components/issues/form.tsx | 381 ++-
web/components/issues/gantt-chart/layout.tsx | 63 -
web/components/issues/index.ts | 8 +-
.../calendar/base-calendar-root.tsx | 117 +
.../issue-layouts/calendar/calendar.tsx | 83 +
.../issue-layouts/calendar/day-tile.tsx | 95 +
.../issue-layouts/calendar/dropdowns/index.ts | 2 +
.../calendar/dropdowns/months-dropdown.tsx | 136 +
.../calendar/dropdowns/options-dropdown.tsx | 137 +
.../issues/issue-layouts/calendar/header.tsx | 98 +
.../issues/issue-layouts/calendar/index.ts | 10 +
.../issue-layouts/calendar/issue-blocks.tsx | 88 +
.../calendar/quick-add-issue-form.tsx | 181 +
.../calendar/roots/cycle-root.tsx | 68 +
.../issue-layouts/calendar/roots/index.ts | 4 +
.../calendar/roots/module-root.tsx | 67 +
.../calendar/roots/project-root.tsx | 58 +
.../calendar/roots/project-view-root.tsx | 59 +
.../issues/issue-layouts/calendar/types.d.ts | 24 +
.../issue-layouts/calendar/week-days.tsx | 63 +
.../issue-layouts/calendar/week-header.tsx | 34 +
.../issue-layouts/empty-states/cycle.tsx | 86 +
.../empty-states/global-view.tsx | 55 +
.../issue-layouts/empty-states/index.ts | 5 +
.../issue-layouts/empty-states/module.tsx | 81 +
.../empty-states/project-view.tsx | 34 +
.../issue-layouts/empty-states/project.tsx | 40 +
.../filters/applied-filters/date.tsx | 54 +
.../filters/applied-filters/filters-list.tsx | 115 +
.../filters/applied-filters/index.ts | 9 +
.../filters/applied-filters/label.tsx | 45 +
.../filters/applied-filters/members.tsx | 40 +
.../filters/applied-filters/priority.tsx | 37 +
.../filters/applied-filters/project.tsx | 49 +
.../applied-filters/roots/archived-issue.tsx | 83 +
.../applied-filters/roots/cycle-root.tsx | 89 +
.../applied-filters/roots/draft-issue.tsx | 78 +
.../roots/global-view-root.tsx | 104 +
.../filters/applied-filters/roots/index.ts | 7 +
.../applied-filters/roots/module-root.tsx | 90 +
.../roots/profile-issues-root.tsx | 73 +
.../applied-filters/roots/project-root.tsx | 74 +
.../roots/project-view-root.tsx | 116 +
.../filters/applied-filters/state-group.tsx | 33 +
.../filters/applied-filters/state.tsx | 41 +
.../display-filters-selection.tsx | 126 +
.../display-filters/display-properties.tsx | 52 +
.../header/display-filters/extra-options.tsx | 47 +
.../header/display-filters/group-by.tsx | 52 +
.../filters/header/display-filters/index.ts | 7 +
.../header/display-filters/issue-type.tsx | 45 +
.../header/display-filters/order-by.tsx | 46 +
.../header/display-filters/sub-group-by.tsx | 51 +
.../filters/header/filters/assignee.tsx | 80 +
.../filters/header/filters/created-by.tsx | 80 +
.../header/filters/filters-selection.tsx | 178 +
.../filters/header/filters/index.ts | 11 +
.../filters/header/filters/labels.tsx | 83 +
.../filters/header/filters/mentions.tsx | 80 +
.../filters/header/filters/priority.tsx | 54 +
.../filters/header/filters/project.tsx | 95 +
.../filters/header/filters/start-date.tsx | 63 +
.../filters/header/filters/state-group.tsx | 71 +
.../filters/header/filters/state.tsx | 78 +
.../filters/header/filters/target-date.tsx | 63 +
.../filters/header/helpers/dropdown.tsx | 72 +
.../filters/header/helpers/filter-header.tsx | 22 +
.../filters/header/helpers/filter-option.tsx | 35 +
.../filters/header/helpers/index.ts | 3 +
.../issue-layouts/filters/header/index.ts | 4 +
.../filters/header/layout-selection.tsx | 42 +
.../issues/issue-layouts/filters/index.ts | 2 +
.../issue-layouts/gantt/base-gantt-root.tsx | 111 +
.../gantt}/blocks.tsx | 33 +-
.../issues/issue-layouts/gantt/cycle-root.tsx | 15 +
.../issues/issue-layouts/gantt/index.ts | 6 +
.../issue-layouts/gantt/module-root.tsx | 15 +
.../issue-layouts/gantt/project-root.tsx | 12 +
.../issue-layouts/gantt/project-view-root.tsx | 11 +
.../gantt/quick-add-issue-form.tsx | 177 +
web/components/issues/issue-layouts/index.ts | 17 +
.../issue-layouts/kanban/base-kanban-root.tsx | 293 ++
.../issues/issue-layouts/kanban/block.tsx | 108 +
.../issue-layouts/kanban/blocks-list.tsx | 69 +
.../issues/issue-layouts/kanban/default.tsx | 448 +++
.../issue-layouts/kanban/headers/assignee.tsx | 74 +
.../kanban/headers/created_by.tsx | 71 +
.../kanban/headers/group-by-card.tsx | 151 +
.../kanban/headers/group-by-root.tsx | 149 +
.../issue-layouts/kanban/headers/label.tsx | 74 +
.../issue-layouts/kanban/headers/priority.tsx | 73 +
.../issue-layouts/kanban/headers/project.tsx | 74 +
.../kanban/headers/state-group.tsx | 77 +
.../issue-layouts/kanban/headers/state.tsx | 71 +
.../kanban/headers/sub-group-by-card.tsx | 40 +
.../kanban/headers/sub-group-by-root.tsx | 134 +
.../issues/issue-layouts/kanban/index.ts | 4 +
.../issue-layouts/kanban/properties.tsx | 197 ++
.../kanban/quick-add-issue-form.tsx | 177 +
.../issue-layouts/kanban/roots/cycle-root.tsx | 87 +
.../kanban/roots/draft-issue-root.tsx | 48 +
.../issue-layouts/kanban/roots/index.ts | 5 +
.../kanban/roots/module-root.tsx | 86 +
.../kanban/roots/profile-issues-root.tsx | 48 +
.../kanban/roots/project-root.tsx | 75 +
.../kanban/roots/project-view-root.tsx | 75 +
.../issues/issue-layouts/kanban/swimlanes.tsx | 604 ++++
.../issue-layouts/list/base-list-root.tsx | 167 +
.../issues/issue-layouts/list/block.tsx | 79 +
.../issues/issue-layouts/list/blocks-list.tsx | 41 +
.../issues/issue-layouts/list/default.tsx | 360 ++
.../issue-layouts/list/headers/assignee.tsx | 41 +
.../issue-layouts/list/headers/created-by.tsx | 38 +
.../list/headers/empty-group.tsx | 29 +
.../list/headers/group-by-card.tsx | 112 +
.../list/headers/group-by-root.tsx | 114 +
.../issue-layouts/list/headers/label.tsx | 41 +
.../issue-layouts/list/headers/priority.tsx | 64 +
.../issue-layouts/list/headers/project.tsx | 41 +
.../list/headers/state-group.tsx | 47 +
.../issue-layouts/list/headers/state.tsx | 38 +
.../issues/issue-layouts/list/index.ts | 5 +
.../issue-layouts/list/list-view-types.d.ts | 6 +
.../issues/issue-layouts/list/properties.tsx | 168 +
.../list/quick-add-issue-form.tsx | 148 +
.../list/roots/archived-issue-root.tsx | 46 +
.../issue-layouts/list/roots/cycle-root.tsx | 55 +
.../list/roots/draft-issue-root.tsx | 48 +
.../issues/issue-layouts/list/roots/index.ts | 6 +
.../issue-layouts/list/roots/module-root.tsx | 56 +
.../list/roots/profile-issues-root.tsx | 49 +
.../issue-layouts/list/roots/project-root.tsx | 48 +
.../list/roots/project-view-root.tsx | 51 +
.../issue-layouts/properties/assignee.tsx | 205 ++
.../issues/issue-layouts/properties/date.tsx | 98 +
.../issue-layouts/properties/estimates.tsx | 175 +
.../issues/issue-layouts/properties/index.tsx | 6 +
.../issue-layouts/properties/labels.tsx | 237 ++
.../issue-layouts/properties/priority.tsx | 25 +
.../issues/issue-layouts/properties/state.tsx | 185 +
.../quick-action-dropdowns/all-issue.tsx | 114 +
.../quick-action-dropdowns/archived-issue.tsx | 71 +
.../quick-action-dropdowns/cycle-issue.tsx | 126 +
.../quick-action-dropdowns/index.ts | 5 +
.../quick-action-dropdowns/module-issue.tsx | 126 +
.../quick-action-dropdowns/project-issue.tsx | 114 +
.../roots/all-issue-layout-root.tsx | 134 +
.../roots/archived-issue-layout-root.tsx | 34 +
.../issue-layouts/roots/cycle-layout-root.tsx | 93 +
.../roots/draft-issue-layout-root.tsx | 51 +
.../issues/issue-layouts/roots/index.ts | 6 +
.../roots/module-layout-root.tsx | 78 +
.../roots/project-layout-root.tsx | 69 +
.../roots/project-view-layout-root.tsx | 68 +
.../spreadsheet/base-spreadsheet-root.tsx | 113 +
.../spreadsheet/columns/assignee-column.tsx | 54 +
.../spreadsheet/columns/attachment-column.tsx | 34 +
.../spreadsheet/columns/columns-list.tsx | 173 +
.../spreadsheet/columns/created-on-column.tsx | 35 +
.../spreadsheet/columns/due-date-column.tsx | 47 +
.../spreadsheet/columns/estimate-column.tsx | 49 +
.../spreadsheet/columns}/index.ts | 9 +-
.../spreadsheet/columns/issue}/index.ts | 0
.../columns/issue/issue-column.tsx | 85 +
.../issue}/spreadsheet-issue-column.tsx | 44 +-
.../spreadsheet/columns/label-column.tsx | 56 +
.../spreadsheet/columns/link-column.tsx | 34 +
.../spreadsheet/columns/priority-column.tsx | 50 +
.../spreadsheet/columns/start-date-column.tsx | 47 +
.../spreadsheet/columns/state-column.tsx | 54 +
.../spreadsheet/columns/sub-issue-column.tsx | 34 +
.../spreadsheet/columns/updated-on-column.tsx | 37 +
.../issues/issue-layouts/spreadsheet/index.ts | 5 +
.../spreadsheet/quick-add-issue-form.tsx | 218 ++
.../spreadsheet/roots/cycle-root.tsx | 43 +
.../issue-layouts/spreadsheet/roots/index.ts | 4 +
.../spreadsheet/roots/module-root.tsx | 44 +
.../spreadsheet/roots/project-root.tsx | 39 +
.../spreadsheet/roots/project-view-root.tsx | 39 +
.../spreadsheet/spreadsheet-column.tsx | 235 ++
.../spreadsheet/spreadsheet-view.tsx | 191 ++
web/components/issues/issue-layouts/types.ts | 5 +
.../issue-peek-overview/activity/card.tsx | 138 +
.../activity/comment-card.tsx | 220 ++
.../activity/comment-editor.tsx | 124 +
.../activity/comment-reaction.tsx | 61 +
.../issue-peek-overview/activity/index.ts | 5 +
.../issue-peek-overview/activity/view.tsx | 60 +
.../issues/issue-peek-overview/index.ts | 1 +
.../issue-peek-overview/issue-detail.tsx | 190 ++
.../issues/issue-peek-overview/properties.tsx | 392 +++
.../issue-peek-overview/reactions/index.ts | 3 +
.../issue-peek-overview/reactions/preview.tsx | 48 +
.../issue-peek-overview/reactions/root.tsx | 30 +
.../reactions/selector.tsx | 69 +
.../issues/issue-peek-overview/root.tsx | 152 +
.../issues/issue-peek-overview/view.tsx | 344 ++
web/components/issues/issue-reaction.tsx | 11 +-
web/components/issues/label.tsx | 11 +-
web/components/issues/main-content.tsx | 110 +-
web/components/issues/modal.tsx | 381 +--
web/components/issues/my-issues/index.ts | 3 -
.../my-issues/my-issues-select-filters.tsx | 221 --
.../my-issues/my-issues-view-options.tsx | 115 -
.../issues/my-issues/my-issues-view.tsx | 329 --
.../issues/parent-issues-list-modal.tsx | 63 +-
.../peek-overview/full-screen-peek-view.tsx | 103 -
.../issues/peek-overview/header.tsx | 125 -
web/components/issues/peek-overview/index.ts | 7 -
.../issues/peek-overview/issue-activity.tsx | 89 -
.../issues/peek-overview/issue-details.tsx | 34 -
.../issues/peek-overview/issue-properties.tsx | 198 --
.../issues/peek-overview/layout.tsx | 193 --
.../issues/peek-overview/side-peek-view.tsx | 92 -
web/components/issues/select/assignee.tsx | 28 +-
web/components/issues/select/cycle.tsx | 153 +
web/components/issues/select/date.tsx | 124 +-
web/components/issues/select/estimate.tsx | 19 +-
web/components/issues/select/index.ts | 2 +
web/components/issues/select/label.tsx | 312 +-
web/components/issues/select/module.tsx | 147 +
web/components/issues/select/priority.tsx | 17 +-
web/components/issues/select/project.tsx | 162 +-
web/components/issues/select/state.tsx | 53 +-
.../issues/sidebar-select/assignee.tsx | 25 +-
.../issues/sidebar-select/blocked.tsx | 34 +-
.../issues/sidebar-select/blocker.tsx | 30 +-
.../issues/sidebar-select/cycle.tsx | 119 +-
.../issues/sidebar-select/duplicate.tsx | 19 +-
.../issues/sidebar-select/estimate.tsx | 30 +-
.../issues/sidebar-select/label.tsx | 418 +--
.../issues/sidebar-select/module.tsx | 124 +-
.../issues/sidebar-select/parent.tsx | 8 +-
.../issues/sidebar-select/priority.tsx | 17 +-
.../issues/sidebar-select/relates-to.tsx | 20 +-
.../issues/sidebar-select/state.tsx | 70 +-
web/components/issues/sidebar.tsx | 278 +-
web/components/issues/sub-issues/issue.tsx | 291 +-
.../issues/sub-issues/issues-list.tsx | 80 +-
.../issues/sub-issues/properties.tsx | 185 +-
web/components/issues/sub-issues/root.tsx | 122 +-
.../issues/view-select/assignee.tsx | 123 -
.../issues/view-select/due-date.tsx | 98 +-
.../issues/view-select/estimate.tsx | 56 +-
web/components/issues/view-select/index.ts | 5 +-
web/components/issues/view-select/label.tsx | 157 -
.../issues/view-select/priority.tsx | 114 -
.../issues/view-select/start-date.tsx | 91 +-
.../workspace-views/workpace-view-issues.tsx | 232 --
.../workspace-views/workspace-all-issue.tsx | 236 --
.../workspace-assigned-issue.tsx | 148 -
.../workspace-created-issues.tsx | 147 -
.../workspace-issue-view-option.tsx | 116 -
.../workspace-subscribed-issue.tsx | 148 -
web/components/labels/create-label-modal.tsx | 115 +-
.../labels/create-update-label-inline.tsx | 161 +-
web/components/labels/delete-label-modal.tsx | 82 +-
web/components/labels/index.ts | 6 +-
.../labels/label-block/drag-handle.tsx | 24 +
.../labels/label-block/label-item-block.tsx | 80 +
.../labels/label-block/label-name.tsx | 27 +
web/components/labels/label-select.tsx | 190 ++
web/components/labels/labels-list-modal.tsx | 97 +-
.../labels/project-setting-label-group.tsx | 166 +
.../labels/project-setting-label-item.tsx | 91 +
.../labels/project-setting-label-list.tsx | 228 ++
web/components/labels/single-label-group.tsx | 189 --
web/components/labels/single-label.tsx | 79 -
.../modules/delete-module-modal.tsx | 86 +-
web/components/modules/form.tsx | 111 +-
web/components/modules/gantt-chart/blocks.tsx | 11 +-
web/components/modules/gantt-chart/index.ts | 1 -
.../gantt-chart/module-issues-layout.tsx | 62 -
.../gantt-chart/modules-list-layout.tsx | 82 +-
web/components/modules/index.ts | 5 +-
web/components/modules/modal.tsx | 98 +-
web/components/modules/module-card-item.tsx | 256 ++
web/components/modules/module-list-item.tsx | 232 ++
.../modules/module-peek-overview.tsx | 55 +
web/components/modules/modules-list-view.tsx | 100 +
web/components/modules/select/lead.tsx | 33 +-
web/components/modules/select/members.tsx | 31 +-
web/components/modules/select/status.tsx | 15 +-
.../modules/sidebar-select/select-lead.tsx | 57 +-
.../modules/sidebar-select/select-members.tsx | 57 +-
.../modules/sidebar-select/select-status.tsx | 17 +-
web/components/modules/sidebar.tsx | 842 +++--
web/components/modules/single-module-card.tsx | 226 --
.../notifications/notification-card.tsx | 205 +-
.../notifications/notification-header.tsx | 27 +-
.../notifications/notification-popover.tsx | 51 +-
.../select-snooze-till-modal.tsx | 71 +-
web/components/onboarding/index.ts | 4 +
web/components/onboarding/invitations.tsx | 171 +
web/components/onboarding/invite-members.tsx | 498 ++-
web/components/onboarding/join-workspaces.tsx | 193 +-
.../onboarding/onboarding-sidebar.tsx | 293 ++
web/components/onboarding/step-indicator.tsx | 19 +
.../switch-delete-account-modal.tsx | 160 +
web/components/onboarding/tour/root.tsx | 77 +-
web/components/onboarding/tour/sidebar.tsx | 27 +-
web/components/onboarding/user-details.tsx | 361 +-
web/components/onboarding/workspace.tsx | 190 +-
web/components/page-views/index.ts | 2 +
web/components/page-views/signin.tsx | 135 +
.../page-views/workspace-dashboard.tsx | 94 +
web/components/pages/create-block.tsx | 79 +-
.../pages/create-update-block-inline.tsx | 234 +-
.../pages/create-update-page-modal.tsx | 138 +-
web/components/pages/delete-page-modal.tsx | 108 +-
web/components/pages/index.ts | 6 +-
web/components/pages/page-form.tsx | 118 +-
.../pages/pages-list/all-pages-list.tsx | 42 +-
.../pages/pages-list/archived-pages-list.tsx | 25 +
.../pages/pages-list/favorite-pages-list.tsx | 44 +-
web/components/pages/pages-list/index.ts | 6 +-
web/components/pages/pages-list/list-item.tsx | 301 ++
web/components/pages/pages-list/list-view.tsx | 71 +
.../pages/pages-list/my-pages-list.tsx | 31 -
.../pages/pages-list/other-pages-list.tsx | 31 -
.../pages/pages-list/private-page-list.tsx | 25 +
.../pages/pages-list/recent-pages-list.tsx | 108 +-
.../pages/pages-list/shared-pages-list.tsx | 25 +
web/components/pages/pages-view.tsx | 296 --
web/components/pages/single-page-block.tsx | 485 ---
.../pages/single-page-detailed-item.tsx | 221 --
.../pages/single-page-list-item.tsx | 218 --
web/components/profile/index.ts | 4 +-
web/components/profile/navbar.tsx | 26 +-
web/components/profile/overview/activity.tsx | 19 +-
.../overview/priority-distribution.tsx | 3 +-
.../profile/overview/state-distribution.tsx | 9 +-
web/components/profile/overview/stats.tsx | 15 +-
.../profile/profile-issues-filter.tsx | 108 +
.../profile/profile-issues-view-options.tsx | 313 --
.../profile/profile-issues-view.tsx | 315 --
web/components/profile/profile-issues.tsx | 62 +
web/components/profile/sidebar.tsx | 57 +-
web/components/project/card-list.tsx | 76 +
web/components/project/card.tsx | 230 ++
.../project/confirm-project-member-remove.tsx | 73 +-
.../project/create-project-modal.tsx | 387 +--
.../project/delete-project-modal.tsx | 133 +-
web/components/project/empty-state.tsx | 52 +
web/components/project/form-loader.tsx | 62 +
web/components/project/form.tsx | 295 ++
web/components/project/index.ts | 23 +-
...egration-card.tsx => integration-card.tsx} | 23 +-
web/components/project/join-project-modal.tsx | 74 +-
web/components/project/label-select.tsx | 245 --
...eave-modal.tsx => leave-project-modal.tsx} | 135 +-
web/components/project/member-list-item.tsx | 184 +
web/components/project/member-list.tsx | 88 +
web/components/project/member-select.tsx | 42 +-
web/components/project/members-select.tsx | 262 +-
web/components/project/priority-select.tsx | 222 +-
.../project-settings-member-defaults.tsx | 145 +
.../project/publish-project/index.tsx | 2 +
.../project/publish-project/modal.tsx | 196 +-
.../project/publish-project/popover.tsx | 12 +-
.../project/send-project-invitation-modal.tsx | 187 +-
web/components/project/settings-sidebar.tsx | 149 -
.../settings/delete-project-section.tsx | 64 +
.../project/settings/features-list.tsx | 113 +
web/components/project/settings/index.ts | 2 +
.../project/settings/single-label.tsx | 148 -
web/components/project/sidebar-list-item.tsx | 344 ++
web/components/project/sidebar-list.tsx | 266 +-
.../project/single-project-card.tsx | 239 --
.../project/single-sidebar-project.tsx | 384 ---
web/components/search-listbox/index.tsx | 165 -
web/components/search-listbox/types.d.ts | 15 -
web/components/states/create-state-modal.tsx | 151 +-
.../states/create-update-state-inline.tsx | 263 +-
web/components/states/delete-state-modal.tsx | 96 +-
web/components/states/index.ts | 3 +-
.../project-setting-state-list-item.tsx | 135 +
.../states/project-setting-state-list.tsx | 188 ++
web/components/states/single-state.tsx | 245 --
web/components/states/state-select.tsx | 230 +-
web/components/tiptap/bubble-menu/index.tsx | 121 -
.../tiptap/bubble-menu/link-selector.tsx | 92 -
.../tiptap/bubble-menu/node-selector.tsx | 130 -
.../bubble-menu/utils/link-validator.tsx | 11 -
web/components/tiptap/extensions/index.tsx | 149 -
.../tiptap/extensions/table/table-cell.ts | 32 -
.../tiptap/extensions/table/table-header.ts | 7 -
.../tiptap/extensions/table/table.ts | 9 -
.../tiptap/extensions/updated-image.tsx | 22 -
web/components/tiptap/index.tsx | 110 -
.../tiptap/plugins/delete-image.tsx | 68 -
.../tiptap/plugins/upload-image.tsx | 127 -
web/components/tiptap/slash-command/index.tsx | 365 --
.../table-menu/InsertBottomTableIcon.tsx | 16 -
.../tiptap/table-menu/InsertLeftTableIcon.tsx | 15 -
.../table-menu/InsertRightTableIcon.tsx | 16 -
.../tiptap/table-menu/InsertTopTableIcon.tsx | 15 -
web/components/tiptap/table-menu/index.tsx | 125 -
web/components/tiptap/utils.ts | 6 -
web/components/toast-alert/index.tsx | 18 +-
web/components/ui/avatar.tsx | 140 -
web/components/ui/buttons/danger-button.tsx | 36 -
web/components/ui/buttons/index.ts | 3 -
web/components/ui/buttons/primary-button.tsx | 32 -
.../ui/buttons/secondary-button.tsx | 32 -
web/components/ui/buttons/type.d.ts | 10 -
web/components/ui/circular-progress.tsx | 39 -
web/components/ui/date.tsx | 43 +-
web/components/ui/datepicker.tsx | 12 +-
web/components/ui/dropdowns/context-menu.tsx | 132 -
web/components/ui/dropdowns/custom-menu.tsx | 173 -
.../ui/dropdowns/custom-search-select.tsx | 186 -
web/components/ui/dropdowns/custom-select.tsx | 118 -
web/components/ui/dropdowns/types.d.ts | 17 -
web/components/ui/empty-space.tsx | 24 +-
web/components/ui/graphs/bar-graph.tsx | 6 +-
web/components/ui/graphs/line-graph.tsx | 6 +-
web/components/ui/graphs/pie-graph.tsx | 2 +-
.../ui/graphs/scatter-plot-graph.tsx | 12 +-
web/components/ui/icon-name-type.d.ts | 2991 -----------------
web/components/ui/index.ts | 14 -
web/components/ui/input/index.tsx | 52 -
web/components/ui/input/types.d.ts | 15 -
.../integration-and-import-export-banner.tsx | 6 +-
web/components/ui/labels-list.tsx | 47 +-
web/components/ui/markdown-to-component.tsx | 4 +-
web/components/ui/multi-level-dropdown.tsx | 29 +-
web/components/ui/multi-level-select.tsx | 18 +-
web/components/ui/product-updates-modal.tsx | 20 +-
web/components/ui/text-area/index.tsx | 85 -
web/components/ui/text-area/types.d.ts | 13 -
web/components/user/index.ts | 1 +
web/components/user/user-greetings.tsx | 49 +
web/components/views/delete-view-modal.tsx | 82 +-
web/components/views/form.tsx | 268 +-
web/components/views/gantt-chart.tsx | 58 -
web/components/views/index.ts | 5 +-
web/components/views/modal.tsx | 132 +-
web/components/views/select-filters.tsx | 275 --
web/components/views/single-view-item.tsx | 175 -
web/components/views/view-list-item.tsx | 136 +
web/components/views/views-list.tsx | 80 +
.../web-hooks/delete-webhook-modal.tsx | 116 +
web/components/web-hooks/empty-state.tsx | 28 +
.../web-hooks/form/delete-section.tsx | 48 +
web/components/web-hooks/form/event-types.tsx | 44 +
web/components/web-hooks/form/form.tsx | 173 +
web/components/web-hooks/form/index.ts | 7 +
.../form/individual-event-options.tsx | 67 +
web/components/web-hooks/form/input.tsx | 26 +
web/components/web-hooks/form/secret-key.tsx | 135 +
web/components/web-hooks/form/toggle.tsx | 28 +
web/components/web-hooks/index.ts | 6 +
web/components/web-hooks/utils.ts | 21 +
.../web-hooks/webhooks-list-item.tsx | 41 +
web/components/web-hooks/webhooks-list.tsx | 19 +
web/components/web-view/activity-message.tsx | 450 ---
web/components/web-view/add-comment.tsx | 133 -
.../web-view/create-update-link-form.tsx | 186 -
web/components/web-view/index.ts | 18 -
web/components/web-view/issue-activity.tsx | 232 --
web/components/web-view/issue-attachments.tsx | 190 --
web/components/web-view/issue-link-list.tsx | 138 -
.../web-view/issue-properties-detail.tsx | 443 ---
.../web-view/issue-web-view-form.tsx | 164 -
web/components/web-view/label.tsx | 7 -
web/components/web-view/select-assignee.tsx | 94 -
web/components/web-view/select-blocked.tsx | 87 -
web/components/web-view/select-blocker.tsx | 87 -
web/components/web-view/select-estimate.tsx | 83 -
web/components/web-view/select-parent.tsx | 76 -
web/components/web-view/select-priority.tsx | 85 -
web/components/web-view/select-state.tsx | 89 -
web/components/web-view/sub-issues.tsx | 108 -
web/components/web-view/web-view-modal.tsx | 108 -
web/components/workspace/activity-graph.tsx | 15 +-
.../workspace/completed-issues-graph.tsx | 3 +-
.../confirm-workspace-member-remove.tsx | 86 +-
.../workspace/create-workspace-form.tsx | 183 +-
.../workspace/delete-workspace-modal.tsx | 117 +-
web/components/workspace/help-section.tsx | 78 +-
web/components/workspace/index.ts | 6 +
web/components/workspace/issues-list.tsx | 29 +-
web/components/workspace/issues-pie-chart.tsx | 8 +-
web/components/workspace/issues-stats.tsx | 8 +-
web/components/workspace/member-select.tsx | 148 +
.../send-workspace-invitation-modal.tsx | 133 +-
web/components/workspace/settings/index.ts | 3 +
.../workspace/settings/members-list-item.tsx | 253 ++
.../workspace/settings/members-list.tsx | 55 +
.../workspace/settings/workspace-details.tsx | 339 ++
web/components/workspace/sidebar-dropdown.tsx | 398 ++-
web/components/workspace/sidebar-menu.tsx | 47 +-
.../workspace/sidebar-quick-action.tsx | 71 +-
.../workspace/single-invitation.tsx | 11 +-
.../views/default-view-list-item.tsx | 36 +
...e-view-modal.tsx => delete-view-modal.tsx} | 94 +-
web/components/workspace/views/form.tsx | 244 +-
.../workspace/views/global-select-filters.tsx | 301 --
web/components/workspace/views/header.tsx | 75 +
web/components/workspace/views/index.ts | 7 +
web/components/workspace/views/modal.tsx | 112 +-
.../views/single-workspace-view-item.tsx | 110 -
.../workspace/views/view-list-item.tsx | 85 +
web/components/workspace/views/views-list.tsx | 50 +
.../views/workpace-view-navigation.tsx | 105 -
web/constants/analytics.ts | 13 +-
web/constants/calendar.ts | 129 +-
web/constants/common.ts | 1 +
web/constants/crisp.tsx | 44 -
web/constants/cycle.ts | 79 +
web/constants/fetch-keys.ts | 174 +-
web/constants/filters.ts | 29 +-
web/constants/issue.ts | 485 ++-
web/constants/kanban-helpers.ts | 19 +
web/constants/module.ts | 69 +-
web/constants/page.ts | 54 +
web/constants/project.ts | 26 +-
web/constants/spreadsheet.ts | 160 +-
web/constants/themes.ts | 13 +-
web/constants/workspace.ts | 97 +-
web/contexts/inbox-view-context.tsx | 197 --
web/contexts/issue-view.context.tsx | 188 +-
web/contexts/profile-issues-context.tsx | 21 +-
web/contexts/project-member.context.tsx | 67 -
web/contexts/theme.context.tsx | 121 -
web/contexts/toast.context.tsx | 6 +-
web/contexts/user-notification-context.tsx | 53 +-
web/contexts/user.context.tsx | 17 +-
web/contexts/workspace-member.context.tsx | 11 +-
web/contexts/workspace-view-context.tsx | 235 --
web/contexts/workspace.context.tsx | 51 -
web/helpers/analytics.helper.ts | 56 +-
web/helpers/array.helper.ts | 47 +-
web/helpers/attachment.helper.ts | 3 +-
web/helpers/calendar.helper.ts | 153 +-
web/helpers/common.helper.ts | 9 +-
web/helpers/date-time.helper.ts | 197 +-
web/helpers/download.helper.ts | 14 +
web/helpers/emoji.helper.tsx | 2 +-
web/helpers/event-tracker.helper.ts | 13 +
web/helpers/filter.helper.ts | 32 +
web/helpers/generate-random-string.ts | 13 +
web/helpers/issue.helper.ts | 199 ++
web/helpers/state.helper.ts | 26 +-
web/helpers/string.helper.ts | 121 +-
web/helpers/user.helper.ts | 12 +
web/hooks/gantt-chart/cycle-issues-view.tsx | 53 -
web/hooks/gantt-chart/issue-view.tsx | 43 -
web/hooks/gantt-chart/module-issues-view.tsx | 53 -
web/hooks/gantt-chart/view-issues-view.tsx | 38 -
web/hooks/my-issues/use-my-issues-filter.tsx | 183 -
web/hooks/my-issues/use-my-issues.tsx | 86 -
web/hooks/use-calendar-issues-view.tsx | 150 -
web/hooks/use-comment-reaction.tsx | 24 +-
web/hooks/use-draggable-portal.ts | 31 +
web/hooks/use-editor-suggestions.tsx | 13 +
web/hooks/use-estimate-option.tsx | 15 +-
web/hooks/use-inbox-view.tsx | 61 -
web/hooks/use-integration-popup.tsx | 25 +-
.../use-issue-notification-subscription.tsx | 22 +-
web/hooks/use-issue-properties.tsx | 112 -
web/hooks/use-issue-reaction.tsx | 26 +-
web/hooks/use-issues-view.tsx | 221 --
web/hooks/use-local-storage.tsx | 4 +-
web/hooks/use-profile-issues.tsx | 127 -
web/hooks/use-project-details.tsx | 8 +-
web/hooks/use-project-members.tsx | 46 -
web/hooks/use-projects.tsx | 42 -
web/hooks/use-spreadsheet-issues-view.tsx | 124 -
web/hooks/use-sub-issue.tsx | 11 +-
web/hooks/use-theme.tsx | 9 -
web/hooks/use-user-auth.tsx | 38 +-
web/hooks/use-user-notifications.tsx | 72 +-
web/hooks/use-user.tsx | 17 +-
web/hooks/use-workspace-details.tsx | 37 -
web/hooks/use-workspace-members.tsx | 45 -
web/hooks/use-workspace-view.tsx | 11 -
web/hooks/use-workspaces.tsx | 27 -
web/layouts/admin-layout/header.tsx | 35 +
web/layouts/admin-layout/index.ts | 3 +
web/layouts/admin-layout/layout.tsx | 44 +
web/layouts/admin-layout/sidebar.tsx | 27 +
web/layouts/app-layout/app-header.tsx | 80 -
web/layouts/app-layout/app-sidebar.tsx | 61 -
web/layouts/app-layout/index.ts | 2 +
web/layouts/app-layout/layout.tsx | 37 +
web/layouts/app-layout/sidebar.tsx | 35 +
web/layouts/auth-layout/admin-wrapper.tsx | 33 +
web/layouts/auth-layout/index.ts | 6 +-
.../project-authorization-wrapper.tsx | 126 -
web/layouts/auth-layout/project-wrapper.tsx | 129 +
.../user-authorization-wrapper.tsx | 40 -
web/layouts/auth-layout/user-wrapper.tsx | 63 +
.../workspace-authorization-wrapper.tsx | 131 -
web/layouts/auth-layout/workspace-wrapper.tsx | 95 +
web/layouts/default-layout/index.tsx | 10 +-
web/layouts/instance-layout/index.tsx | 54 +
web/layouts/profile-layout.tsx | 45 -
web/layouts/settings-layout/index.ts | 3 +
web/layouts/settings-layout/profile/index.ts | 2 +
.../settings-layout/profile/layout.tsx | 30 +
.../settings-layout/profile/sidebar.tsx | 241 ++
web/layouts/settings-layout/project/index.ts | 2 +
.../settings-layout/project/layout.tsx | 20 +
.../settings-layout/project/sidebar.tsx | 68 +
.../settings-layout/workspace/index.ts | 2 +
.../settings-layout/workspace/layout.tsx | 20 +
.../settings-layout/workspace/sidebar.tsx | 47 +
.../user-profile-layout}/index.ts | 1 -
web/layouts/user-profile-layout/layout.tsx | 41 +
web/layouts/web-view-layout/index.tsx | 64 -
web/lib/app-provider.tsx | 59 +
web/lib/auth.ts | 200 --
web/lib/cookie.ts | 16 -
web/lib/local-storage.ts | 21 +
web/lib/mobx/store-init.tsx | 53 -
web/lib/wrappers/crisp-wrapper.tsx | 40 +
web/lib/wrappers/posthog-wrapper.tsx | 72 +
web/lib/wrappers/store-wrapper.tsx | 101 +
web/next.config.js | 25 +-
web/package.json | 68 +-
web/pages/404.tsx | 14 +-
web/pages/[workspaceSlug]/analytics.tsx | 211 +-
web/pages/[workspaceSlug]/editor.tsx | 192 --
web/pages/[workspaceSlug]/index.tsx | 211 +-
web/pages/[workspaceSlug]/me/my-issues.tsx | 126 -
.../[workspaceSlug]/me/profile/activity.tsx | 227 --
.../[workspaceSlug]/me/profile/index.tsx | 394 ---
.../me/profile/preferences.tsx | 98 -
.../profile/[userId]/assigned.tsx | 33 +-
.../profile/[userId]/created.tsx | 33 +-
.../profile/[userId]/index.tsx | 53 +-
.../profile/[userId]/subscribed.tsx | 33 +-
.../archived-issues/[archivedIssueId].tsx | 94 +-
.../[projectId]/archived-issues/index.tsx | 83 +-
.../projects/[projectId]/cycles/[cycleId].tsx | 250 +-
.../projects/[projectId]/cycles/index.tsx | 371 +-
.../[projectId]/draft-issues/index.tsx | 80 +-
.../projects/[projectId]/inbox/[inboxId].tsx | 87 +-
.../projects/[projectId]/issues/[issueId].tsx | 74 +-
.../projects/[projectId]/issues/index.tsx | 119 +-
.../[projectId]/modules/[moduleId].tsx | 233 +-
.../projects/[projectId]/modules/index.tsx | 192 +-
.../projects/[projectId]/pages/[pageId].tsx | 897 ++---
.../projects/[projectId]/pages/index.tsx | 304 +-
.../[projectId]/settings/automations.tsx | 120 +-
.../[projectId]/settings/estimates.tsx | 203 +-
.../[projectId]/settings/features.tsx | 235 +-
.../projects/[projectId]/settings/index.tsx | 489 +--
.../[projectId]/settings/integrations.tsx | 144 +-
.../projects/[projectId]/settings/labels.tsx | 223 +-
.../projects/[projectId]/settings/members.tsx | 519 +--
.../projects/[projectId]/settings/states.tsx | 178 +-
.../projects/[projectId]/views/[viewId].tsx | 146 +-
.../projects/[projectId]/views/index.tsx | 152 +-
web/pages/[workspaceSlug]/projects/index.tsx | 175 +-
.../[workspaceSlug]/settings/api-tokens.tsx | 91 +
.../[workspaceSlug]/settings/billing.tsx | 103 +-
.../[workspaceSlug]/settings/exports.tsx | 84 +-
.../[workspaceSlug]/settings/imports.tsx | 81 +-
web/pages/[workspaceSlug]/settings/index.tsx | 375 +--
.../[workspaceSlug]/settings/integrations.tsx | 107 +-
.../[workspaceSlug]/settings/members.tsx | 427 +--
.../settings/webhooks/[webhookId].tsx | 76 +
.../settings/webhooks/create.tsx | 43 +
.../settings/webhooks/index.tsx | 80 +
.../workspace-views/[globalViewId].tsx | 24 +
.../workspace-views/all-issues.tsx | 56 +-
.../workspace-views/assigned.tsx | 56 +-
.../workspace-views/created.tsx | 56 +-
.../[workspaceSlug]/workspace-views/index.tsx | 221 +-
.../workspace-views/issues.tsx | 40 -
.../workspace-views/subscribed.tsx | 54 +-
web/pages/_app.tsx | 38 +-
web/pages/_document.tsx | 4 +-
web/pages/_error.tsx | 24 +-
web/pages/accounts/password.tsx | 217 ++
web/pages/{ => accounts}/sign-up.tsx | 46 +-
web/pages/api/slack-redirect.ts | 23 -
web/pages/api/track-event.ts | 34 -
web/pages/api/unsplash.ts | 26 -
web/pages/create-workspace.tsx | 131 +-
web/pages/error.tsx | 16 -
web/pages/god-mode/ai.tsx | 64 +
web/pages/god-mode/authorization.tsx | 185 +
web/pages/god-mode/email.tsx | 54 +
web/pages/god-mode/image.tsx | 50 +
web/pages/god-mode/index.tsx | 52 +
web/pages/index.tsx | 233 +-
web/pages/installations/[provider]/index.tsx | 99 +-
web/pages/invitations.tsx | 225 --
web/pages/invitations/index.tsx | 234 ++
web/pages/m/[workspaceSlug]/editor.tsx | 99 -
.../projects/[projectId]/issues/[issueId].tsx | 175 -
web/pages/magic-sign-in.tsx | 108 -
web/pages/onboarding.tsx | 235 --
web/pages/onboarding/index.tsx | 227 ++
web/pages/profile/activity.tsx | 196 ++
web/pages/profile/change-password.tsx | 189 ++
web/pages/profile/index.tsx | 440 +++
web/pages/profile/preferences.tsx | 80 +
web/pages/reset-password.tsx | 163 -
web/pages/workspace-invitations/index.tsx | 143 +
web/pages/workspace-member-invitation.tsx | 153 -
web/postcss.config.js | 9 +-
web/public/auth/access-denied.svg | 49 +
web/public/emoji/project-emoji.svg | 4 +
.../empty-state/Project_full_screen.svg | 31 +
web/public/empty-state/api-token.svg | 49 +
.../empty-state/dashboard_empty_project.webp | Bin 0 -> 31886 bytes
web/public/empty-state/empty_analytics.webp | Bin 0 -> 12182 bytes
web/public/empty-state/empty_cycles.webp | Bin 0 -> 27484 bytes
web/public/empty-state/empty_issues.webp | Bin 0 -> 60102 bytes
web/public/empty-state/empty_label.svg | 4 +
web/public/empty-state/empty_members.svg | 13 +
web/public/empty-state/empty_modules.webp | Bin 0 -> 18618 bytes
web/public/empty-state/empty_page.webp | Bin 0 -> 47164 bytes
web/public/empty-state/empty_project.webp | Bin 0 -> 35082 bytes
web/public/empty-state/empty_view.webp | Bin 0 -> 30928 bytes
web/public/empty-state/state_graph.svg | 12 +-
web/public/empty-state/web-hook.svg | 49 +
web/public/instance-not-ready.svg | 57 +
web/public/instance-setup-done.svg | 60 +
.../instance/plane-instance-not-ready.webp | Bin 0 -> 45894 bytes
web/public/logos/github-dark.svg | 3 +
web/public/onboarding/onboarding-issues.svg | 592 ++++
web/public/onboarding/onboarding-pages.svg | 62 +
web/public/onboarding/sign-in.svg | 649 ++++
web/public/onboarding/user-dark.svg | 14 +
web/public/onboarding/user-light.svg | 14 +
web/public/services/jira.png | Bin 231268 -> 0 bytes
web/public/services/jira.svg | 15 +
web/public/services/json.svg | 2 +-
web/public/users/user-1.png | Bin 0 -> 4193 bytes
web/public/users/user-2.png | Bin 0 -> 3781 bytes
web/public/web-view-spinner.png | Bin 0 -> 1456 bytes
web/services/ai.service.ts | 17 +-
web/services/analytics.service.ts | 7 +-
web/services/api.service.ts | 64 +-
web/services/api_token.service.ts | 41 +
...onfig.service.ts => app_config.service.ts} | 16 +-
...service.ts => app_installation.service.ts} | 20 +-
web/services/auth.service.ts | 160 +
web/services/authentication.service.ts | 84 -
web/services/cycle.service.ts | 129 +
web/services/cycles.service.ts | 208 --
web/services/estimates.service.ts | 90 -
web/services/file.service.ts | 70 +-
web/services/inbox.service.ts | 92 +-
web/services/instance.service.ts | 66 +
web/services/integration/csv.services.ts | 36 -
web/services/integration/github.service.ts | 55 -
web/services/integration/jira.service.ts | 40 -
web/services/integrations/github.service.ts | 39 +
web/services/integrations/index.ts | 3 +
.../integration.service.ts} | 34 +-
web/services/integrations/jira.service.ts | 28 +
web/services/issue/index.ts | 7 +
web/services/issue/issue.service.ts | 230 ++
web/services/issue/issue_archive.service.ts | 43 +
.../issue/issue_attachment.service.ts | 49 +
web/services/issue/issue_comment.service.ts | 59 +
web/services/issue/issue_draft.service.tsx | 52 +
web/services/issue/issue_label.service.ts | 51 +
web/services/issue/issue_reaction.service.ts | 82 +
web/services/issues.service.ts | 690 ----
web/services/module.service.ts | 200 ++
web/services/modules.service.ts | 277 --
...ons.service.ts => notification.service.ts} | 84 +-
.../{pages.service.ts => page.service.ts} | 167 +-
web/services/project-publish.service.ts | 106 -
web/services/project.service.ts | 385 ---
web/services/project/index.ts | 6 +
.../project/project-estimate.service.ts | 57 +
.../project/project-export.service.ts | 23 +
.../project/project-member.service.ts | 64 +
.../project/project-publish.service.ts | 61 +
web/services/project/project-state.service.ts | 68 +
web/services/project/project.service.ts | 164 +
web/services/reaction.service.ts | 136 -
web/services/state.service.ts | 112 -
web/services/track-event.service.ts | 955 ------
web/services/user.service.ts | 154 +-
web/services/view.service.ts | 81 +
web/services/views.service.ts | 141 -
web/services/web-waitlist.service.ts | 22 -
web/services/webhook.service.ts | 60 +
web/services/workspace.service.ts | 138 +-
web/store/app-config.store.ts | 47 +
web/store/archived-issues/index.ts | 3 +
web/store/archived-issues/issue.store.ts | 230 ++
.../archived-issues/issue_detail.store.ts | 198 ++
.../archived-issues/issue_filters.store.ts | 247 ++
web/store/calendar.store.ts | 121 +
web/store/command-palette.store.ts | 200 ++
web/store/cycle-issues/index.ts | 1 +
web/store/cycle-issues/issue_filters.store.ts | 201 ++
web/store/cycle/cycle_issue.store.ts | 356 ++
.../cycle/cycle_issue_calendar_view.store.ts | 89 +
web/store/cycle/cycle_issue_filters.store.ts | 147 +
.../cycle/cycle_issue_kanban_view.store.ts | 448 +++
web/store/cycle/cycles.store.ts | 427 +++
web/store/cycle/index.ts | 5 +
web/store/draft-issues/index.ts | 2 +
web/store/draft-issues/issue.store.ts | 184 +
web/store/draft-issues/issue_filters.store.ts | 109 +
web/store/editor/index.ts | 1 +
web/store/editor/mentions.store.ts | 46 +
web/store/event-tracker.store.ts | 80 +
.../global-view/global_view_filters.store.ts | 68 +
.../global-view/global_view_issues.store.ts | 204 ++
web/store/global-view/global_views.store.ts | 205 ++
web/store/global-view/index.ts | 3 +
web/store/inbox/inbox.store.ts | 162 +
web/store/inbox/inbox_filters.store.ts | 149 +
web/store/inbox/inbox_issue_detail.store.ts | 254 ++
web/store/inbox/inbox_issues.store.ts | 93 +
web/store/inbox/index.ts | 4 +
web/store/instance/index.ts | 1 +
web/store/instance/instance.store.ts | 196 ++
web/store/issue/index.ts | 7 +
web/store/issue/issue.store.ts | 323 ++
web/store/issue/issue_calendar_view.store.ts | 88 +
web/store/issue/issue_detail.store.ts | 644 ++++
.../issue_draft.store.ts} | 52 +-
web/store/issue/issue_filters.store.ts | 249 ++
web/store/issue/issue_kanban_view.store.ts | 448 +++
web/store/issue/issue_quick_add.store.ts | 123 +
web/store/issues.ts | 173 -
.../base-issue-calendar-helper.store.ts | 53 +
.../issues/base-issue-kanban-helper.store.ts | 192 ++
web/store/issues/global/filter.store.ts | 432 +++
web/store/issues/global/issue.store.ts | 218 ++
web/store/issues/index.ts | 48 +
web/store/issues/profile/filter.store.ts | 341 ++
web/store/issues/profile/issue.store.ts | 334 ++
.../project-issues/archived/filter.store.ts | 145 +
.../project-issues/archived/issue.store.ts | 157 +
.../project-issues/base-issue-filter.store.ts | 29 +
.../issues/project-issues/base-issue.store.ts | 207 ++
.../project-issues/cycle/filter.store.ts | 258 ++
.../project-issues/cycle/issue.store.ts | 351 ++
.../project-issues/draft/filter.store.ts | 142 +
.../project-issues/draft/issue.store.ts | 188 ++
.../project-issues/issue-filters.store.ts | 252 ++
.../project-issues/module/filter.store.ts | 266 ++
.../project-issues/module/issue.store.ts | 356 ++
.../project-view/filter.store.ts | 260 ++
.../project-view/issue.store.ts | 224 ++
.../project-issues/project/filter.store.ts | 145 +
.../project-issues/project/issue.store.ts | 224 ++
web/store/issues/project-issues/utils.ts | 5 +
web/store/issues/types.ts | 33 +
web/store/module-issues/index.ts | 1 +
.../module-issues/issue_filters.store.ts | 201 ++
web/store/module/index.ts | 5 +
web/store/module/module_filters.store.ts | 170 +
web/store/module/module_issue.store.ts | 371 ++
.../module_issue_calendar_view.store.ts | 89 +
.../module/module_issue_kanban_view.store.ts | 448 +++
web/store/module/modules.store.ts | 391 +++
web/store/page.store.ts | 365 ++
web/store/profile-issues/index.ts | 2 +
web/store/profile-issues/issue.store.ts | 282 ++
.../profile-issues/issue_filters.store.ts | 137 +
web/store/project-view/index.ts | 5 +
.../project_view_filters.store.ts | 68 +
.../project_view_issue_calendar_view.store.ts | 89 +
.../project-view/project_view_issues.store.ts | 379 +++
web/store/project-view/project_views.store.ts | 282 ++
web/store/project.ts | 86 -
web/store/project/index.ts | 6 +
web/store/project/project-estimates.store.ts | 188 ++
web/store/project/project-label.store.ts | 255 ++
web/store/project/project-members.store.ts | 201 ++
.../project-publish.store.ts} | 215 +-
web/store/project/project-state.store.ts | 266 ++
web/store/project/project.store.ts | 354 ++
web/store/root.ts | 399 ++-
web/store/{theme.ts => theme.store.ts} | 38 +-
web/store/user.store.ts | 449 +++
web/store/user.ts | 133 -
web/store/webhook.store.ts | 207 ++
web/store/workspace/index.ts | 3 +
web/store/workspace/workspace-member.store.ts | 292 ++
web/store/workspace/workspace.store.ts | 296 ++
.../workspace/workspace_filters.store.ts | 193 ++
web/styles/editor.css | 113 +-
web/styles/globals.css | 197 +-
web/styles/table.css | 206 ++
web/tailwind.config.js | 6 +-
web/types/analytics.d.ts | 52 +-
web/types/api_token.d.ts | 16 +
web/types/app.d.ts | 17 +
web/types/auth.d.ts | 22 +
web/types/cycles.d.ts | 21 +-
web/types/inbox.d.ts | 36 +-
web/types/index.d.ts | 2 +
web/types/instance.d.ts | 47 +
web/types/issues.d.ts | 18 +-
web/types/modules.d.ts | 13 +-
web/types/pages.d.ts | 15 +-
web/types/projects.d.ts | 34 +-
web/types/users.d.ts | 144 +-
web/types/view-props.d.ts | 73 +-
web/types/views.d.ts | 19 +-
web/types/webhook.d.ts | 15 +
web/types/workspace-views.d.ts | 8 +-
web/types/workspace.d.ts | 33 +-
yarn.lock | 2963 +++++++++-------
1861 files changed, 119160 insertions(+), 77659 deletions(-)
create mode 100644 .deepsource.toml
create mode 100644 .github/workflows/build-branch.yml
create mode 100644 ENV_SETUP.md
create mode 100644 apiserver/Dockerfile.dev
delete mode 100644 apiserver/bin/user_script.py
create mode 100644 apiserver/package.json
create mode 100644 apiserver/plane/api/middleware/__init__.py
create mode 100644 apiserver/plane/api/middleware/api_authentication.py
delete mode 100644 apiserver/plane/api/permissions/__init__.py
create mode 100644 apiserver/plane/api/rate_limit.py
delete mode 100644 apiserver/plane/api/serializers/api_token.py
create mode 100644 apiserver/plane/api/urls/__init__.py
create mode 100644 apiserver/plane/api/urls/cycle.py
create mode 100644 apiserver/plane/api/urls/inbox.py
create mode 100644 apiserver/plane/api/urls/issue.py
create mode 100644 apiserver/plane/api/urls/module.py
create mode 100644 apiserver/plane/api/urls/project.py
create mode 100644 apiserver/plane/api/urls/state.py
delete mode 100644 apiserver/plane/api/views/analytic.py
delete mode 100644 apiserver/plane/api/views/api_token.py
delete mode 100644 apiserver/plane/api/views/asset.py
delete mode 100644 apiserver/plane/api/views/auth_extended.py
delete mode 100644 apiserver/plane/api/views/authentication.py
delete mode 100644 apiserver/plane/api/views/config.py
delete mode 100644 apiserver/plane/api/views/estimate.py
delete mode 100644 apiserver/plane/api/views/exporter.py
delete mode 100644 apiserver/plane/api/views/external.py
delete mode 100644 apiserver/plane/api/views/importer.py
delete mode 100644 apiserver/plane/api/views/integration/base.py
delete mode 100644 apiserver/plane/api/views/integration/github.py
delete mode 100644 apiserver/plane/api/views/integration/slack.py
delete mode 100644 apiserver/plane/api/views/notification.py
delete mode 100644 apiserver/plane/api/views/oauth.py
delete mode 100644 apiserver/plane/api/views/page.py
delete mode 100644 apiserver/plane/api/views/user.py
delete mode 100644 apiserver/plane/api/views/view.py
delete mode 100644 apiserver/plane/api/views/workspace.py
create mode 100644 apiserver/plane/app/__init__.py
create mode 100644 apiserver/plane/app/apps.py
create mode 100644 apiserver/plane/app/middleware/__init__.py
create mode 100644 apiserver/plane/app/middleware/api_authentication.py
create mode 100644 apiserver/plane/app/permissions/__init__.py
rename apiserver/plane/{api => app}/permissions/project.py (87%)
rename apiserver/plane/{api => app}/permissions/workspace.py (68%)
create mode 100644 apiserver/plane/app/serializers/__init__.py
rename apiserver/plane/{api => app}/serializers/analytic.py (91%)
create mode 100644 apiserver/plane/app/serializers/api.py
rename apiserver/plane/{api => app}/serializers/asset.py (100%)
create mode 100644 apiserver/plane/app/serializers/base.py
create mode 100644 apiserver/plane/app/serializers/cycle.py
rename apiserver/plane/{api => app}/serializers/estimate.py (94%)
rename apiserver/plane/{api => app}/serializers/exporter.py (100%)
rename apiserver/plane/{api => app}/serializers/importer.py (100%)
create mode 100644 apiserver/plane/app/serializers/inbox.py
rename apiserver/plane/{api => app}/serializers/integration/__init__.py (83%)
rename apiserver/plane/{api => app}/serializers/integration/base.py (90%)
rename apiserver/plane/{api => app}/serializers/integration/github.py (95%)
rename apiserver/plane/{api => app}/serializers/integration/slack.py (86%)
create mode 100644 apiserver/plane/app/serializers/issue.py
create mode 100644 apiserver/plane/app/serializers/module.py
rename apiserver/plane/{api => app}/serializers/notification.py (100%)
rename apiserver/plane/{api => app}/serializers/page.py (73%)
create mode 100644 apiserver/plane/app/serializers/project.py
create mode 100644 apiserver/plane/app/serializers/state.py
create mode 100644 apiserver/plane/app/serializers/user.py
rename apiserver/plane/{api => app}/serializers/view.py (96%)
create mode 100644 apiserver/plane/app/serializers/webhook.py
create mode 100644 apiserver/plane/app/serializers/workspace.py
create mode 100644 apiserver/plane/app/urls/__init__.py
create mode 100644 apiserver/plane/app/urls/analytic.py
create mode 100644 apiserver/plane/app/urls/api.py
create mode 100644 apiserver/plane/app/urls/asset.py
create mode 100644 apiserver/plane/app/urls/authentication.py
create mode 100644 apiserver/plane/app/urls/config.py
create mode 100644 apiserver/plane/app/urls/cycle.py
create mode 100644 apiserver/plane/app/urls/estimate.py
create mode 100644 apiserver/plane/app/urls/external.py
create mode 100644 apiserver/plane/app/urls/importer.py
create mode 100644 apiserver/plane/app/urls/inbox.py
create mode 100644 apiserver/plane/app/urls/integration.py
create mode 100644 apiserver/plane/app/urls/issue.py
create mode 100644 apiserver/plane/app/urls/module.py
create mode 100644 apiserver/plane/app/urls/notification.py
create mode 100644 apiserver/plane/app/urls/page.py
create mode 100644 apiserver/plane/app/urls/project.py
create mode 100644 apiserver/plane/app/urls/search.py
create mode 100644 apiserver/plane/app/urls/state.py
create mode 100644 apiserver/plane/app/urls/user.py
create mode 100644 apiserver/plane/app/urls/views.py
create mode 100644 apiserver/plane/app/urls/webhook.py
create mode 100644 apiserver/plane/app/urls/workspace.py
rename apiserver/plane/{api/urls.py => app/urls_deprecated.py} (95%)
create mode 100644 apiserver/plane/app/views/__init__.py
create mode 100644 apiserver/plane/app/views/analytic.py
create mode 100644 apiserver/plane/app/views/api.py
create mode 100644 apiserver/plane/app/views/asset.py
create mode 100644 apiserver/plane/app/views/auth_extended.py
create mode 100644 apiserver/plane/app/views/authentication.py
create mode 100644 apiserver/plane/app/views/base.py
create mode 100644 apiserver/plane/app/views/config.py
create mode 100644 apiserver/plane/app/views/cycle.py
create mode 100644 apiserver/plane/app/views/estimate.py
create mode 100644 apiserver/plane/app/views/exporter.py
create mode 100644 apiserver/plane/app/views/external.py
create mode 100644 apiserver/plane/app/views/importer.py
create mode 100644 apiserver/plane/app/views/inbox.py
rename apiserver/plane/{api => app}/views/integration/__init__.py (100%)
create mode 100644 apiserver/plane/app/views/integration/base.py
create mode 100644 apiserver/plane/app/views/integration/github.py
create mode 100644 apiserver/plane/app/views/integration/slack.py
create mode 100644 apiserver/plane/app/views/issue.py
create mode 100644 apiserver/plane/app/views/module.py
create mode 100644 apiserver/plane/app/views/notification.py
create mode 100644 apiserver/plane/app/views/oauth.py
create mode 100644 apiserver/plane/app/views/page.py
create mode 100644 apiserver/plane/app/views/project.py
rename apiserver/plane/{api => app}/views/search.py (53%)
create mode 100644 apiserver/plane/app/views/state.py
create mode 100644 apiserver/plane/app/views/user.py
create mode 100644 apiserver/plane/app/views/view.py
create mode 100644 apiserver/plane/app/views/webhook.py
create mode 100644 apiserver/plane/app/views/workspace.py
delete mode 100644 apiserver/plane/bgtasks/email_verification_task.py
create mode 100644 apiserver/plane/bgtasks/event_tracking_task.py
create mode 100644 apiserver/plane/bgtasks/file_asset_task.py
create mode 100644 apiserver/plane/bgtasks/notification_task.py
create mode 100644 apiserver/plane/bgtasks/user_count_task.py
create mode 100644 apiserver/plane/bgtasks/webhook_task.py
create mode 100644 apiserver/plane/db/management/commands/create_bucket.py
create mode 100644 apiserver/plane/db/migrations/0046_label_sort_order_alter_analyticview_created_by_and_more.py
create mode 100644 apiserver/plane/db/migrations/0047_webhook_apitoken_description_apitoken_expired_at_and_more.py
create mode 100644 apiserver/plane/db/migrations/0048_auto_20231116_0713.py
create mode 100644 apiserver/plane/db/migrations/0049_auto_20231116_0713.py
create mode 100644 apiserver/plane/db/migrations/0050_user_use_case_alter_workspace_organization_size.py
create mode 100644 apiserver/plane/db/models/api.py
delete mode 100644 apiserver/plane/db/models/api_token.py
create mode 100644 apiserver/plane/db/models/webhook.py
create mode 100644 apiserver/plane/license/__init__.py
create mode 100644 apiserver/plane/license/api/__init__.py
create mode 100644 apiserver/plane/license/api/permissions/__init__.py
create mode 100644 apiserver/plane/license/api/permissions/instance.py
create mode 100644 apiserver/plane/license/api/serializers/__init__.py
create mode 100644 apiserver/plane/license/api/serializers/instance.py
create mode 100644 apiserver/plane/license/api/views/__init__.py
create mode 100644 apiserver/plane/license/api/views/instance.py
create mode 100644 apiserver/plane/license/apps.py
create mode 100644 apiserver/plane/license/management/__init__.py
create mode 100644 apiserver/plane/license/management/commands/__init__.py
create mode 100644 apiserver/plane/license/management/commands/configure_instance.py
create mode 100644 apiserver/plane/license/management/commands/register_instance.py
create mode 100644 apiserver/plane/license/migrations/0001_initial.py
create mode 100644 apiserver/plane/license/migrations/__init__.py
create mode 100644 apiserver/plane/license/models/__init__.py
create mode 100644 apiserver/plane/license/models/instance.py
create mode 100644 apiserver/plane/license/urls.py
create mode 100644 apiserver/plane/license/utils/__init__.py
create mode 100644 apiserver/plane/license/utils/encryption.py
create mode 100644 apiserver/plane/license/utils/instance_value.py
create mode 100644 apiserver/plane/middleware/api_log_middleware.py
delete mode 100644 apiserver/plane/middleware/user_middleware.py
delete mode 100644 apiserver/plane/settings/selfhosted.py
delete mode 100644 apiserver/plane/settings/staging.py
create mode 100644 apiserver/plane/space/__init__.py
create mode 100644 apiserver/plane/space/apps.py
create mode 100644 apiserver/plane/space/serializer/__init__.py
create mode 100644 apiserver/plane/space/serializer/base.py
create mode 100644 apiserver/plane/space/serializer/cycle.py
create mode 100644 apiserver/plane/space/serializer/inbox.py
create mode 100644 apiserver/plane/space/serializer/issue.py
create mode 100644 apiserver/plane/space/serializer/module.py
create mode 100644 apiserver/plane/space/serializer/project.py
create mode 100644 apiserver/plane/space/serializer/state.py
create mode 100644 apiserver/plane/space/serializer/user.py
create mode 100644 apiserver/plane/space/serializer/workspace.py
create mode 100644 apiserver/plane/space/urls/__init__.py
create mode 100644 apiserver/plane/space/urls/inbox.py
create mode 100644 apiserver/plane/space/urls/issue.py
create mode 100644 apiserver/plane/space/urls/project.py
create mode 100644 apiserver/plane/space/views/__init__.py
create mode 100644 apiserver/plane/space/views/base.py
create mode 100644 apiserver/plane/space/views/inbox.py
create mode 100644 apiserver/plane/space/views/issue.py
create mode 100644 apiserver/plane/space/views/project.py
create mode 100644 apiserver/plane/utils/integrations/slack.py
delete mode 100644 apiserver/templates/emails/auth/email_verification.html
create mode 100644 deploy/coolify/README.md
create mode 100644 deploy/coolify/coolify-docker-compose.yml
delete mode 100644 deploy/heroku/Dockerfile
create mode 100644 deploy/kubernetes/README.md
create mode 100644 deploy/selfhost/README.md
create mode 100644 deploy/selfhost/docker-compose.yml
create mode 100644 deploy/selfhost/images/download.png
create mode 100644 deploy/selfhost/images/migrate-error.png
create mode 100644 deploy/selfhost/images/restart.png
create mode 100644 deploy/selfhost/images/started.png
create mode 100644 deploy/selfhost/images/stopped.png
create mode 100644 deploy/selfhost/images/upgrade.png
create mode 100755 deploy/selfhost/install-private.sh
create mode 100755 deploy/selfhost/install.sh
create mode 100755 deploy/selfhost/migration-0.13-0.14.sh
create mode 100644 deploy/selfhost/variables.env
delete mode 100644 docker-compose-hub.yml
create mode 100644 docker-compose-local.yml
create mode 100644 packages/editor/core/Readme.md
create mode 100644 packages/editor/core/package.json
create mode 100644 packages/editor/core/postcss.config.js
create mode 100644 packages/editor/core/src/index.ts
create mode 100644 packages/editor/core/src/lib/editor-commands.ts
create mode 100644 packages/editor/core/src/lib/utils.ts
create mode 100644 packages/editor/core/src/styles/editor.css
create mode 100644 packages/editor/core/src/styles/github-dark.css
create mode 100644 packages/editor/core/src/styles/tailwind.css
create mode 100644 packages/editor/core/src/ui/components/editor-container.tsx
create mode 100644 packages/editor/core/src/ui/components/editor-content.tsx
create mode 100644 packages/editor/core/src/ui/extensions/code/index.tsx
create mode 100644 packages/editor/core/src/ui/extensions/custom-list-keymap/index.ts
create mode 100644 packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/find-list-item-pos.ts
create mode 100644 packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/get-next-list-depth.ts
create mode 100644 packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/handle-backspace.ts
create mode 100644 packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/handle-delete.ts
create mode 100644 packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/has-list-before.ts
create mode 100644 packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/has-list-item-after.ts
create mode 100644 packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/has-list-item-before.ts
create mode 100644 packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/index.ts
create mode 100644 packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/next-list-is-deeper.ts
create mode 100644 packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/next-list-is-higher.ts
create mode 100644 packages/editor/core/src/ui/extensions/custom-list-keymap/list-keymap.ts
create mode 100644 packages/editor/core/src/ui/extensions/horizontal-rule.tsx
rename {web/components/tiptap/extensions => packages/editor/core/src/ui/extensions/image}/image-resize.tsx (87%)
create mode 100644 packages/editor/core/src/ui/extensions/image/index.tsx
create mode 100644 packages/editor/core/src/ui/extensions/image/read-only-image.tsx
create mode 100644 packages/editor/core/src/ui/extensions/index.tsx
create mode 100644 packages/editor/core/src/ui/extensions/keymap.tsx
create mode 100644 packages/editor/core/src/ui/extensions/table/table-cell/index.ts
create mode 100644 packages/editor/core/src/ui/extensions/table/table-cell/table-cell.ts
create mode 100644 packages/editor/core/src/ui/extensions/table/table-header/index.ts
create mode 100644 packages/editor/core/src/ui/extensions/table/table-header/table-header.ts
create mode 100644 packages/editor/core/src/ui/extensions/table/table-row/index.ts
create mode 100644 packages/editor/core/src/ui/extensions/table/table-row/table-row.ts
create mode 100644 packages/editor/core/src/ui/extensions/table/table/icons.ts
create mode 100644 packages/editor/core/src/ui/extensions/table/table/index.ts
create mode 100644 packages/editor/core/src/ui/extensions/table/table/table-controls.ts
create mode 100644 packages/editor/core/src/ui/extensions/table/table/table-view.tsx
create mode 100644 packages/editor/core/src/ui/extensions/table/table/table.ts
create mode 100644 packages/editor/core/src/ui/extensions/table/table/utilities/create-cell.ts
create mode 100644 packages/editor/core/src/ui/extensions/table/table/utilities/create-table.ts
create mode 100644 packages/editor/core/src/ui/extensions/table/table/utilities/delete-table-when-all-cells-selected.ts
create mode 100644 packages/editor/core/src/ui/extensions/table/table/utilities/get-table-node-types.ts
create mode 100644 packages/editor/core/src/ui/extensions/table/table/utilities/is-cell-selection.ts
create mode 100644 packages/editor/core/src/ui/hooks/useEditor.tsx
create mode 100644 packages/editor/core/src/ui/hooks/useInitializedContent.tsx
create mode 100644 packages/editor/core/src/ui/hooks/useReadOnlyEditor.tsx
create mode 100644 packages/editor/core/src/ui/index.tsx
create mode 100644 packages/editor/core/src/ui/mentions/MentionList.tsx
create mode 100644 packages/editor/core/src/ui/mentions/custom.tsx
create mode 100644 packages/editor/core/src/ui/mentions/index.tsx
create mode 100644 packages/editor/core/src/ui/mentions/mentionNodeView.tsx
create mode 100644 packages/editor/core/src/ui/mentions/suggestion.ts
create mode 100644 packages/editor/core/src/ui/menus/menu-items/index.tsx
rename {space/components/tiptap => packages/editor/core/src/ui}/plugins/delete-image.tsx (63%)
create mode 100644 packages/editor/core/src/ui/plugins/upload-image.tsx
rename {web/components/tiptap => packages/editor/core/src/ui}/props.tsx (68%)
create mode 100644 packages/editor/core/src/ui/read-only/extensions.tsx
create mode 100644 packages/editor/core/src/ui/read-only/props.tsx
create mode 100644 packages/editor/core/tailwind.config.js
create mode 100644 packages/editor/core/tsconfig.json
create mode 100644 packages/editor/core/tsup.config.ts
create mode 100644 packages/editor/document-editor/Readme.md
create mode 100644 packages/editor/document-editor/package.json
create mode 100644 packages/editor/document-editor/postcss.config.js
create mode 100644 packages/editor/document-editor/src/index.ts
create mode 100644 packages/editor/document-editor/src/ui/components/alert-label.tsx
create mode 100644 packages/editor/document-editor/src/ui/components/content-browser.tsx
create mode 100644 packages/editor/document-editor/src/ui/components/editor-header.tsx
create mode 100644 packages/editor/document-editor/src/ui/components/heading-component.tsx
create mode 100644 packages/editor/document-editor/src/ui/components/index.ts
create mode 100644 packages/editor/document-editor/src/ui/components/info-popover.tsx
create mode 100644 packages/editor/document-editor/src/ui/components/page-renderer.tsx
create mode 100644 packages/editor/document-editor/src/ui/components/summary-popover.tsx
create mode 100644 packages/editor/document-editor/src/ui/components/summary-side-bar.tsx
create mode 100644 packages/editor/document-editor/src/ui/components/vertical-dropdown-menu.tsx
create mode 100644 packages/editor/document-editor/src/ui/extensions/index.tsx
create mode 100644 packages/editor/document-editor/src/ui/hooks/use-editor-markings.tsx
create mode 100644 packages/editor/document-editor/src/ui/index.tsx
create mode 100644 packages/editor/document-editor/src/ui/menu/fixed-menu.tsx
rename {web/components/ui => packages/editor/document-editor/src/ui/menu}/icon.tsx (66%)
create mode 100644 packages/editor/document-editor/src/ui/menu/index.tsx
create mode 100644 packages/editor/document-editor/src/ui/readonly/index.tsx
rename {web/components => packages/editor/document-editor/src}/ui/tooltip.tsx (84%)
create mode 100644 packages/editor/document-editor/src/ui/types/editor-types.ts
create mode 100644 packages/editor/document-editor/src/ui/types/menu-actions.d.ts
create mode 100644 packages/editor/document-editor/src/ui/utils/editor-summary-utils.ts
create mode 100644 packages/editor/document-editor/src/ui/utils/menu-actions.ts
create mode 100644 packages/editor/document-editor/src/ui/utils/menu-options.ts
create mode 100644 packages/editor/document-editor/tailwind.config.js
create mode 100644 packages/editor/document-editor/tsconfig.json
create mode 100644 packages/editor/document-editor/tsup.config.ts
create mode 100644 packages/editor/extensions/Readme.md
create mode 100644 packages/editor/extensions/package.json
create mode 100644 packages/editor/extensions/postcss.config.js
create mode 100644 packages/editor/extensions/src/extensions/drag-drop.tsx
rename space/components/tiptap/slash-command/index.tsx => packages/editor/extensions/src/extensions/slash-commands.tsx (79%)
create mode 100644 packages/editor/extensions/src/index.ts
create mode 100644 packages/editor/extensions/tailwind.config.js
create mode 100644 packages/editor/extensions/tsconfig.json
create mode 100644 packages/editor/extensions/tsup.config.ts
create mode 100644 packages/editor/lite-text-editor/Readme.md
create mode 100644 packages/editor/lite-text-editor/package.json
create mode 100644 packages/editor/lite-text-editor/postcss.config.js
create mode 100644 packages/editor/lite-text-editor/src/index.ts
create mode 100644 packages/editor/lite-text-editor/src/ui/extensions/enter-key-extension.tsx
create mode 100644 packages/editor/lite-text-editor/src/ui/extensions/index.tsx
create mode 100644 packages/editor/lite-text-editor/src/ui/index.tsx
create mode 100644 packages/editor/lite-text-editor/src/ui/menus/fixed-menu/icon.tsx
create mode 100644 packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx
create mode 100644 packages/editor/lite-text-editor/src/ui/read-only/index.tsx
create mode 100644 packages/editor/lite-text-editor/src/ui/tooltip.tsx
create mode 100644 packages/editor/lite-text-editor/tailwind.config.js
create mode 100644 packages/editor/lite-text-editor/tsconfig.json
create mode 100644 packages/editor/lite-text-editor/tsup.config.ts
create mode 100644 packages/editor/rich-text-editor/Readme.md
create mode 100644 packages/editor/rich-text-editor/package.json
create mode 100644 packages/editor/rich-text-editor/postcss.config.js
create mode 100644 packages/editor/rich-text-editor/src/index.ts
create mode 100644 packages/editor/rich-text-editor/src/ui/extensions/index.tsx
create mode 100644 packages/editor/rich-text-editor/src/ui/index.tsx
create mode 100644 packages/editor/rich-text-editor/src/ui/menus/bubble-menu/index.tsx
rename {space/components/tiptap => packages/editor/rich-text-editor/src/ui/menus}/bubble-menu/link-selector.tsx (86%)
rename {space/components/tiptap => packages/editor/rich-text-editor/src/ui/menus}/bubble-menu/node-selector.tsx (52%)
create mode 100644 packages/editor/rich-text-editor/src/ui/read-only/index.tsx
create mode 100644 packages/editor/rich-text-editor/tailwind.config.js
create mode 100644 packages/editor/rich-text-editor/tsconfig.json
create mode 100644 packages/editor/rich-text-editor/tsup.config.ts
create mode 100644 packages/editor/types/Readme.md
create mode 100644 packages/editor/types/package.json
create mode 100644 packages/editor/types/postcss.config.js
create mode 100644 packages/editor/types/src/index.ts
create mode 100644 packages/editor/types/src/types/delete-image.ts
create mode 100644 packages/editor/types/src/types/mention-suggestion.ts
create mode 100644 packages/editor/types/src/types/restore-image.ts
create mode 100644 packages/editor/types/src/types/upload-image.ts
create mode 100644 packages/editor/types/tailwind.config.js
create mode 100644 packages/editor/types/tsconfig.json
create mode 100644 packages/editor/types/tsup.config.ts
delete mode 100644 packages/ui/button/index.tsx
delete mode 100644 packages/ui/index.tsx
create mode 100644 packages/ui/src/avatar/avatar-group.tsx
create mode 100644 packages/ui/src/avatar/avatar.tsx
create mode 100644 packages/ui/src/avatar/index.ts
create mode 100644 packages/ui/src/badge/badge.tsx
create mode 100644 packages/ui/src/badge/helper.tsx
create mode 100644 packages/ui/src/badge/index.ts
create mode 100644 packages/ui/src/breadcrumbs/breadcrumbs.tsx
create mode 100644 packages/ui/src/breadcrumbs/index.ts
create mode 100644 packages/ui/src/button/button.tsx
create mode 100644 packages/ui/src/button/helper.tsx
create mode 100644 packages/ui/src/button/index.ts
rename {web/components/ui => packages/ui/src/button}/toggle-switch.tsx (87%)
create mode 100644 packages/ui/src/dropdowns/custom-menu.tsx
create mode 100644 packages/ui/src/dropdowns/custom-search-select.tsx
create mode 100644 packages/ui/src/dropdowns/custom-select.tsx
create mode 100644 packages/ui/src/dropdowns/helper.tsx
rename {web/components/ui => packages/ui/src}/dropdowns/index.ts (63%)
create mode 100644 packages/ui/src/form-fields/index.ts
create mode 100644 packages/ui/src/form-fields/input-color-picker.tsx
create mode 100644 packages/ui/src/form-fields/input.tsx
create mode 100644 packages/ui/src/form-fields/textarea.tsx
create mode 100644 packages/ui/src/icons/admin-profile-icon.tsx
create mode 100644 packages/ui/src/icons/archive-icon.tsx
create mode 100644 packages/ui/src/icons/blocked-icon.tsx
create mode 100644 packages/ui/src/icons/blocker-icon.tsx
create mode 100644 packages/ui/src/icons/calendar-after-icon.tsx
create mode 100644 packages/ui/src/icons/calendar-before-icon.tsx
create mode 100644 packages/ui/src/icons/center-panel-icon.tsx
create mode 100644 packages/ui/src/icons/copy-icon.tsx
create mode 100644 packages/ui/src/icons/create-icon.tsx
create mode 100644 packages/ui/src/icons/cycle/circle-dot-full-icon.tsx
create mode 100644 packages/ui/src/icons/cycle/contrast-icon.tsx
create mode 100644 packages/ui/src/icons/cycle/cycle-group-icon.tsx
create mode 100644 packages/ui/src/icons/cycle/double-circle-icon.tsx
create mode 100644 packages/ui/src/icons/cycle/helper.tsx
create mode 100644 packages/ui/src/icons/cycle/index.ts
create mode 100644 packages/ui/src/icons/dice-icon.tsx
create mode 100644 packages/ui/src/icons/discord-icon.tsx
create mode 100644 packages/ui/src/icons/external-link-icon.tsx
create mode 100644 packages/ui/src/icons/full-screen-panel-icon.tsx
create mode 100644 packages/ui/src/icons/github-icon.tsx
create mode 100644 packages/ui/src/icons/index.ts
create mode 100644 packages/ui/src/icons/layer-stack.tsx
create mode 100644 packages/ui/src/icons/layers-icon.tsx
create mode 100644 packages/ui/src/icons/module/backlog.tsx
create mode 100644 packages/ui/src/icons/module/cancelled.tsx
create mode 100644 packages/ui/src/icons/module/completed.tsx
create mode 100644 packages/ui/src/icons/module/in-progress.tsx
create mode 100644 packages/ui/src/icons/module/index.ts
create mode 100644 packages/ui/src/icons/module/module-status-icon.tsx
create mode 100644 packages/ui/src/icons/module/paused.tsx
create mode 100644 packages/ui/src/icons/module/planned.tsx
create mode 100644 packages/ui/src/icons/photo-filter-icon.tsx
create mode 100644 packages/ui/src/icons/priority-icon.tsx
create mode 100644 packages/ui/src/icons/related-icon.tsx
create mode 100644 packages/ui/src/icons/running-icon.tsx
create mode 100644 packages/ui/src/icons/side-panel-icon.tsx
create mode 100644 packages/ui/src/icons/state/backlog-group-icon.tsx
create mode 100644 packages/ui/src/icons/state/cancelled-group-icon.tsx
create mode 100644 packages/ui/src/icons/state/completed-group-icon.tsx
create mode 100644 packages/ui/src/icons/state/helper.tsx
create mode 100644 packages/ui/src/icons/state/index.ts
create mode 100644 packages/ui/src/icons/state/started-group-icon.tsx
create mode 100644 packages/ui/src/icons/state/state-group-icon.tsx
create mode 100644 packages/ui/src/icons/state/unstarted-group-icon.tsx
create mode 100644 packages/ui/src/icons/subscribe-icon.tsx
create mode 100644 packages/ui/src/icons/transfer-icon.tsx
create mode 100644 packages/ui/src/icons/type.d.ts
create mode 100644 packages/ui/src/icons/user-group-icon.tsx
create mode 100644 packages/ui/src/index.ts
rename {web/components/ui => packages/ui/src}/loader.tsx (74%)
create mode 100644 packages/ui/src/progress/circular-progress-indicator.tsx
create mode 100644 packages/ui/src/progress/index.ts
rename {web/components/ui => packages/ui/src/progress}/linear-progress-indicator.tsx (75%)
rename {web/components/ui => packages/ui/src/progress}/progress-bar.tsx (80%)
create mode 100644 packages/ui/src/progress/radial-progress.tsx
rename web/components/ui/spinner.tsx => packages/ui/src/spinners/circular-spinner.tsx (77%)
create mode 100644 packages/ui/src/spinners/index.ts
create mode 100644 packages/ui/src/tooltip/index.ts
create mode 100644 packages/ui/src/tooltip/tooltip.tsx
create mode 100644 space/Dockerfile.dev
delete mode 100644 space/components/icons/index.ts
delete mode 100644 space/components/icons/state-group/backlog-state-icon.tsx
delete mode 100644 space/components/icons/state-group/cancelled-state-icon.tsx
delete mode 100644 space/components/icons/state-group/completed-state-icon.tsx
delete mode 100644 space/components/icons/state-group/index.ts
delete mode 100644 space/components/icons/state-group/started-state-icon.tsx
delete mode 100644 space/components/icons/state-group/state-group-icon.tsx
delete mode 100644 space/components/icons/state-group/unstarted-state-icon.tsx
delete mode 100644 space/components/icons/types.d.ts
delete mode 100644 space/components/issues/navbar/issue-view.tsx
delete mode 100644 space/components/issues/navbar/search.tsx
delete mode 100644 space/components/tiptap/bubble-menu/index.tsx
delete mode 100644 space/components/tiptap/bubble-menu/utils/link-validator.tsx
delete mode 100644 space/components/tiptap/extensions/image-resize.tsx
delete mode 100644 space/components/tiptap/extensions/index.tsx
delete mode 100644 space/components/tiptap/extensions/table/table-cell.ts
delete mode 100644 space/components/tiptap/extensions/table/table-header.ts
delete mode 100644 space/components/tiptap/extensions/table/table.ts
delete mode 100644 space/components/tiptap/extensions/updated-image.tsx
delete mode 100644 space/components/tiptap/index.tsx
delete mode 100644 space/components/tiptap/plugins/upload-image.tsx
delete mode 100644 space/components/tiptap/props.tsx
delete mode 100644 space/components/tiptap/table-menu/InsertBottomTableIcon.tsx
delete mode 100644 space/components/tiptap/table-menu/InsertLeftTableIcon.tsx
delete mode 100644 space/components/tiptap/table-menu/InsertRightTableIcon.tsx
delete mode 100644 space/components/tiptap/table-menu/InsertTopTableIcon.tsx
delete mode 100644 space/components/tiptap/table-menu/index.tsx
delete mode 100644 space/components/tiptap/utils.ts
create mode 100644 space/hooks/use-editor-suggestions.tsx
create mode 100644 space/store/mentions.store.ts
create mode 100644 space/styles/table.css
create mode 100644 web/components/account/deactivate-account-modal.tsx
delete mode 100644 web/components/account/email-code-form.tsx
delete mode 100644 web/components/account/email-password-form.tsx
delete mode 100644 web/components/account/email-reset-password-form.tsx
create mode 100644 web/components/account/sign-in-forms/create-password.tsx
create mode 100644 web/components/account/sign-in-forms/email-form.tsx
create mode 100644 web/components/account/sign-in-forms/index.ts
create mode 100644 web/components/account/sign-in-forms/o-auth-options.tsx
create mode 100644 web/components/account/sign-in-forms/optional-set-password.tsx
create mode 100644 web/components/account/sign-in-forms/password.tsx
create mode 100644 web/components/account/sign-in-forms/root.tsx
create mode 100644 web/components/account/sign-in-forms/set-password-link.tsx
create mode 100644 web/components/account/sign-in-forms/unique-code.tsx
delete mode 100644 web/components/analytics/custom-analytics/create-update-analytics-modal.tsx
create mode 100644 web/components/analytics/custom-analytics/main-content.tsx
rename web/components/analytics/{ => custom-analytics}/select/index.ts (100%)
rename web/components/analytics/{ => custom-analytics}/select/project.tsx (79%)
rename web/components/analytics/{ => custom-analytics}/select/segment.tsx (85%)
rename web/components/analytics/{ => custom-analytics}/select/x-axis.tsx (64%)
rename web/components/analytics/{ => custom-analytics}/select/y-axis.tsx (93%)
delete mode 100644 web/components/analytics/custom-analytics/sidebar.tsx
create mode 100644 web/components/analytics/custom-analytics/sidebar/index.ts
create mode 100644 web/components/analytics/custom-analytics/sidebar/projects-list.tsx
create mode 100644 web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx
create mode 100644 web/components/analytics/custom-analytics/sidebar/sidebar.tsx
delete mode 100644 web/components/analytics/project-modal.tsx
create mode 100644 web/components/analytics/project-modal/header.tsx
create mode 100644 web/components/analytics/project-modal/index.ts
create mode 100644 web/components/analytics/project-modal/main-content.tsx
create mode 100644 web/components/analytics/project-modal/modal.tsx
create mode 100644 web/components/api-token/delete-token-modal.tsx
create mode 100644 web/components/api-token/empty-state.tsx
create mode 100644 web/components/api-token/index.ts
create mode 100644 web/components/api-token/modal/create-token-modal.tsx
create mode 100644 web/components/api-token/modal/form.tsx
create mode 100644 web/components/api-token/modal/generated-token-details.tsx
create mode 100644 web/components/api-token/modal/index.ts
create mode 100644 web/components/api-token/token-list-item.tsx
create mode 100644 web/components/command-palette/actions/help-actions.tsx
create mode 100644 web/components/command-palette/actions/index.ts
create mode 100644 web/components/command-palette/actions/issue-actions/actions-list.tsx
create mode 100644 web/components/command-palette/actions/issue-actions/change-assignee.tsx
create mode 100644 web/components/command-palette/actions/issue-actions/change-priority.tsx
create mode 100644 web/components/command-palette/actions/issue-actions/change-state.tsx
create mode 100644 web/components/command-palette/actions/issue-actions/index.ts
create mode 100644 web/components/command-palette/actions/project-actions.tsx
create mode 100644 web/components/command-palette/actions/search-results.tsx
create mode 100644 web/components/command-palette/actions/theme-actions.tsx
create mode 100644 web/components/command-palette/actions/workspace-settings-actions.tsx
delete mode 100644 web/components/command-palette/change-interface-theme.tsx
delete mode 100644 web/components/command-palette/command-k.tsx
create mode 100644 web/components/command-palette/command-modal.tsx
delete mode 100644 web/components/command-palette/issue/change-issue-assignee.tsx
delete mode 100644 web/components/command-palette/issue/change-issue-priority.tsx
delete mode 100644 web/components/command-palette/issue/change-issue-state.tsx
delete mode 100644 web/components/command-palette/issue/index.ts
delete mode 100644 web/components/command-palette/shortcuts-modal.tsx
create mode 100644 web/components/command-palette/shortcuts-modal/commands-list.tsx
create mode 100644 web/components/command-palette/shortcuts-modal/index.ts
create mode 100644 web/components/command-palette/shortcuts-modal/modal.tsx
rename web/components/{ui => common}/empty-state.tsx (70%)
create mode 100644 web/components/common/index.ts
create mode 100644 web/components/common/latest-feature-block.tsx
create mode 100644 web/components/common/new-empty-state.tsx
create mode 100644 web/components/common/product-updates-modal.tsx
delete mode 100644 web/components/core/filters/filters-list.tsx
delete mode 100644 web/components/core/filters/issues-view-filter.tsx
delete mode 100644 web/components/core/filters/workspace-filters-list.tsx
create mode 100644 web/components/core/modals/user-image-upload-modal.tsx
rename web/components/core/modals/{image-upload-modal.tsx => workspace-image-upload-modal.tsx} (66%)
delete mode 100644 web/components/core/views/all-views.tsx
delete mode 100644 web/components/core/views/board-view/all-boards.tsx
delete mode 100644 web/components/core/views/board-view/board-header.tsx
delete mode 100644 web/components/core/views/board-view/index.ts
delete mode 100644 web/components/core/views/board-view/inline-create-issue-form.tsx
delete mode 100644 web/components/core/views/board-view/single-board.tsx
delete mode 100644 web/components/core/views/board-view/single-issue.tsx
delete mode 100644 web/components/core/views/calendar-view/calendar-header.tsx
delete mode 100644 web/components/core/views/calendar-view/calendar.tsx
delete mode 100644 web/components/core/views/calendar-view/index.ts
delete mode 100644 web/components/core/views/calendar-view/inline-create-issue-form.tsx
delete mode 100644 web/components/core/views/calendar-view/single-date.tsx
delete mode 100644 web/components/core/views/calendar-view/single-issue.tsx
delete mode 100644 web/components/core/views/gantt-chart-view/index.tsx
delete mode 100644 web/components/core/views/gantt-chart-view/inline-create-issue-form.tsx
delete mode 100644 web/components/core/views/index.ts
delete mode 100644 web/components/core/views/inline-issue-create-wrapper.tsx
delete mode 100644 web/components/core/views/issues-view.tsx
delete mode 100644 web/components/core/views/list-view/all-lists.tsx
delete mode 100644 web/components/core/views/list-view/index.ts
delete mode 100644 web/components/core/views/list-view/inline-create-issue-form.tsx
delete mode 100644 web/components/core/views/list-view/single-issue.tsx
delete mode 100644 web/components/core/views/list-view/single-list.tsx
delete mode 100644 web/components/core/views/spreadsheet-view/assignee-column/assignee-column.tsx
delete mode 100644 web/components/core/views/spreadsheet-view/assignee-column/index.ts
delete mode 100644 web/components/core/views/spreadsheet-view/assignee-column/spreadsheet-assignee-column.tsx
delete mode 100644 web/components/core/views/spreadsheet-view/created-on-column/created-on-column.tsx
delete mode 100644 web/components/core/views/spreadsheet-view/created-on-column/index.ts
delete mode 100644 web/components/core/views/spreadsheet-view/created-on-column/spreadsheet-created-on-column.tsx
delete mode 100644 web/components/core/views/spreadsheet-view/due-date-column/due-date-column.tsx
delete mode 100644 web/components/core/views/spreadsheet-view/due-date-column/index.ts
delete mode 100644 web/components/core/views/spreadsheet-view/due-date-column/spreadsheet-due-date-column.tsx
delete mode 100644 web/components/core/views/spreadsheet-view/estimate-column/estimate-column.tsx
delete mode 100644 web/components/core/views/spreadsheet-view/estimate-column/index.ts
delete mode 100644 web/components/core/views/spreadsheet-view/estimate-column/spreadsheet-estimate-column.tsx
delete mode 100644 web/components/core/views/spreadsheet-view/issue-column/issue-column.tsx
delete mode 100644 web/components/core/views/spreadsheet-view/label-column/index.ts
delete mode 100644 web/components/core/views/spreadsheet-view/label-column/label-column.tsx
delete mode 100644 web/components/core/views/spreadsheet-view/label-column/spreadsheet-label-column.tsx
delete mode 100644 web/components/core/views/spreadsheet-view/priority-column/index.ts
delete mode 100644 web/components/core/views/spreadsheet-view/priority-column/priority-column.tsx
delete mode 100644 web/components/core/views/spreadsheet-view/priority-column/spreadsheet-priority-column.tsx
delete mode 100644 web/components/core/views/spreadsheet-view/single-issue.tsx
delete mode 100644 web/components/core/views/spreadsheet-view/spreadsheet-view.tsx
delete mode 100644 web/components/core/views/spreadsheet-view/start-date-column/index.ts
delete mode 100644 web/components/core/views/spreadsheet-view/start-date-column/spreadsheet-start-date-column.tsx
delete mode 100644 web/components/core/views/spreadsheet-view/start-date-column/start-date-column.tsx
delete mode 100644 web/components/core/views/spreadsheet-view/state-column/index.ts
delete mode 100644 web/components/core/views/spreadsheet-view/state-column/spreadsheet-state-column.tsx
delete mode 100644 web/components/core/views/spreadsheet-view/state-column/state-column.tsx
delete mode 100644 web/components/core/views/spreadsheet-view/updated-on-column/index.ts
delete mode 100644 web/components/core/views/spreadsheet-view/updated-on-column/spreadsheet-updated-on-column.tsx
delete mode 100644 web/components/core/views/spreadsheet-view/updated-on-column/updated-on-column.tsx
create mode 100644 web/components/cycles/cycle-peek-overview.tsx
create mode 100644 web/components/cycles/cycles-board-card.tsx
create mode 100644 web/components/cycles/cycles-board.tsx
create mode 100644 web/components/cycles/cycles-list-item.tsx
create mode 100644 web/components/cycles/cycles-list.tsx
delete mode 100644 web/components/cycles/cycles-list/all-cycles-list.tsx
delete mode 100644 web/components/cycles/cycles-list/completed-cycles-list.tsx
delete mode 100644 web/components/cycles/cycles-list/draft-cycles-list.tsx
delete mode 100644 web/components/cycles/cycles-list/index.ts
delete mode 100644 web/components/cycles/cycles-list/upcoming-cycles-list.tsx
delete mode 100644 web/components/cycles/delete-cycle-modal.tsx
create mode 100644 web/components/cycles/delete-modal.tsx
delete mode 100644 web/components/cycles/gantt-chart/cycle-issues-layout.tsx
delete mode 100644 web/components/cycles/select.tsx
delete mode 100644 web/components/cycles/single-cycle-card.tsx
delete mode 100644 web/components/cycles/single-cycle-list.tsx
rename web/components/estimates/{single-estimate.tsx => estimate-list-item.tsx} (50%)
create mode 100644 web/components/estimates/estimate-select.tsx
create mode 100644 web/components/estimates/estimates-list.tsx
create mode 100644 web/components/estimates/index.ts
delete mode 100644 web/components/estimates/index.tsx
delete mode 100644 web/components/gantt-chart/hooks/block-update.tsx
rename web/components/gantt-chart/{sidebar.tsx => sidebar/cycle-sidebar.tsx} (81%)
create mode 100644 web/components/gantt-chart/sidebar/index.ts
create mode 100644 web/components/gantt-chart/sidebar/module-sidebar.tsx
create mode 100644 web/components/gantt-chart/sidebar/project-view-sidebar.tsx
create mode 100644 web/components/gantt-chart/sidebar/sidebar.tsx
create mode 100644 web/components/headers/cycle-issues.tsx
create mode 100644 web/components/headers/cycles.tsx
create mode 100644 web/components/headers/global-issues.tsx
create mode 100644 web/components/headers/index.ts
create mode 100644 web/components/headers/module-issues.tsx
create mode 100644 web/components/headers/modules-list.tsx
create mode 100644 web/components/headers/page-details.tsx
create mode 100644 web/components/headers/pages.tsx
create mode 100644 web/components/headers/profile-preferences.tsx
create mode 100644 web/components/headers/profile-settings.tsx
create mode 100644 web/components/headers/project-archived-issue-details.tsx
create mode 100644 web/components/headers/project-archived-issues.tsx
create mode 100644 web/components/headers/project-draft-issues.tsx
create mode 100644 web/components/headers/project-inbox.tsx
create mode 100644 web/components/headers/project-issue-details.tsx
create mode 100644 web/components/headers/project-issues.tsx
create mode 100644 web/components/headers/project-settings.tsx
create mode 100644 web/components/headers/project-view-issues.tsx
create mode 100644 web/components/headers/project-views.tsx
create mode 100644 web/components/headers/projects.tsx
create mode 100644 web/components/headers/user-profile.tsx
create mode 100644 web/components/headers/workspace-analytics.tsx
create mode 100644 web/components/headers/workspace-dashboard.tsx
create mode 100644 web/components/headers/workspace-settings.tsx
rename web/components/inbox/{inbox-action-headers.tsx => actions-header.tsx} (51%)
delete mode 100644 web/components/inbox/inbox-issue-card.tsx
rename web/components/inbox/{inbox-issue-activity.tsx => issue-activity.tsx} (55%)
create mode 100644 web/components/inbox/issue-card.tsx
rename web/components/inbox/{inbox-main-content.tsx => main-content.tsx} (58%)
rename web/components/inbox/{ => modals}/accept-issue-modal.tsx (74%)
create mode 100644 web/components/inbox/modals/create-issue-modal.tsx
rename web/components/inbox/{ => modals}/decline-issue-modal.tsx (72%)
rename web/components/inbox/{ => modals}/delete-issue-modal.tsx (66%)
create mode 100644 web/components/inbox/modals/index.ts
rename web/components/inbox/{ => modals}/select-duplicate.tsx (76%)
create mode 100644 web/components/instance/ai-form.tsx
create mode 100644 web/components/instance/email-form.tsx
create mode 100644 web/components/instance/general-form.tsx
create mode 100644 web/components/instance/github-config-form.tsx
create mode 100644 web/components/instance/google-config-form.tsx
create mode 100644 web/components/instance/help-section.tsx
create mode 100644 web/components/instance/image-config-form.tsx
create mode 100644 web/components/instance/index.ts
create mode 100644 web/components/instance/instance-admin-restriction.tsx
create mode 100644 web/components/instance/not-ready-view.tsx
create mode 100644 web/components/instance/setup-done-view.tsx
create mode 100644 web/components/instance/setup-form/email-code-form.tsx
create mode 100644 web/components/instance/setup-form/email-form.tsx
create mode 100644 web/components/instance/setup-form/index.ts
create mode 100644 web/components/instance/setup-form/password-form.tsx
create mode 100644 web/components/instance/setup-form/root.tsx
create mode 100644 web/components/instance/setup-view.tsx
create mode 100644 web/components/instance/sidebar-dropdown.tsx
create mode 100644 web/components/instance/sidebar-menu.tsx
create mode 100644 web/components/issues/delete-archived-issue-modal.tsx
delete mode 100644 web/components/issues/gantt-chart/layout.tsx
create mode 100644 web/components/issues/issue-layouts/calendar/base-calendar-root.tsx
create mode 100644 web/components/issues/issue-layouts/calendar/calendar.tsx
create mode 100644 web/components/issues/issue-layouts/calendar/day-tile.tsx
create mode 100644 web/components/issues/issue-layouts/calendar/dropdowns/index.ts
create mode 100644 web/components/issues/issue-layouts/calendar/dropdowns/months-dropdown.tsx
create mode 100644 web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx
create mode 100644 web/components/issues/issue-layouts/calendar/header.tsx
create mode 100644 web/components/issues/issue-layouts/calendar/index.ts
create mode 100644 web/components/issues/issue-layouts/calendar/issue-blocks.tsx
create mode 100644 web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx
create mode 100644 web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx
create mode 100644 web/components/issues/issue-layouts/calendar/roots/index.ts
create mode 100644 web/components/issues/issue-layouts/calendar/roots/module-root.tsx
create mode 100644 web/components/issues/issue-layouts/calendar/roots/project-root.tsx
create mode 100644 web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx
create mode 100644 web/components/issues/issue-layouts/calendar/types.d.ts
create mode 100644 web/components/issues/issue-layouts/calendar/week-days.tsx
create mode 100644 web/components/issues/issue-layouts/calendar/week-header.tsx
create mode 100644 web/components/issues/issue-layouts/empty-states/cycle.tsx
create mode 100644 web/components/issues/issue-layouts/empty-states/global-view.tsx
create mode 100644 web/components/issues/issue-layouts/empty-states/index.ts
create mode 100644 web/components/issues/issue-layouts/empty-states/module.tsx
create mode 100644 web/components/issues/issue-layouts/empty-states/project-view.tsx
create mode 100644 web/components/issues/issue-layouts/empty-states/project.tsx
create mode 100644 web/components/issues/issue-layouts/filters/applied-filters/date.tsx
create mode 100644 web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx
create mode 100644 web/components/issues/issue-layouts/filters/applied-filters/index.ts
create mode 100644 web/components/issues/issue-layouts/filters/applied-filters/label.tsx
create mode 100644 web/components/issues/issue-layouts/filters/applied-filters/members.tsx
create mode 100644 web/components/issues/issue-layouts/filters/applied-filters/priority.tsx
create mode 100644 web/components/issues/issue-layouts/filters/applied-filters/project.tsx
create mode 100644 web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx
create mode 100644 web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx
create mode 100644 web/components/issues/issue-layouts/filters/applied-filters/roots/draft-issue.tsx
create mode 100644 web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx
create mode 100644 web/components/issues/issue-layouts/filters/applied-filters/roots/index.ts
create mode 100644 web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx
create mode 100644 web/components/issues/issue-layouts/filters/applied-filters/roots/profile-issues-root.tsx
create mode 100644 web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx
create mode 100644 web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx
create mode 100644 web/components/issues/issue-layouts/filters/applied-filters/state-group.tsx
create mode 100644 web/components/issues/issue-layouts/filters/applied-filters/state.tsx
create mode 100644 web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx
create mode 100644 web/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx
create mode 100644 web/components/issues/issue-layouts/filters/header/display-filters/extra-options.tsx
create mode 100644 web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx
create mode 100644 web/components/issues/issue-layouts/filters/header/display-filters/index.ts
create mode 100644 web/components/issues/issue-layouts/filters/header/display-filters/issue-type.tsx
create mode 100644 web/components/issues/issue-layouts/filters/header/display-filters/order-by.tsx
create mode 100644 web/components/issues/issue-layouts/filters/header/display-filters/sub-group-by.tsx
create mode 100644 web/components/issues/issue-layouts/filters/header/filters/assignee.tsx
create mode 100644 web/components/issues/issue-layouts/filters/header/filters/created-by.tsx
create mode 100644 web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx
create mode 100644 web/components/issues/issue-layouts/filters/header/filters/index.ts
create mode 100644 web/components/issues/issue-layouts/filters/header/filters/labels.tsx
create mode 100644 web/components/issues/issue-layouts/filters/header/filters/mentions.tsx
create mode 100644 web/components/issues/issue-layouts/filters/header/filters/priority.tsx
create mode 100644 web/components/issues/issue-layouts/filters/header/filters/project.tsx
create mode 100644 web/components/issues/issue-layouts/filters/header/filters/start-date.tsx
create mode 100644 web/components/issues/issue-layouts/filters/header/filters/state-group.tsx
create mode 100644 web/components/issues/issue-layouts/filters/header/filters/state.tsx
create mode 100644 web/components/issues/issue-layouts/filters/header/filters/target-date.tsx
create mode 100644 web/components/issues/issue-layouts/filters/header/helpers/dropdown.tsx
create mode 100644 web/components/issues/issue-layouts/filters/header/helpers/filter-header.tsx
create mode 100644 web/components/issues/issue-layouts/filters/header/helpers/filter-option.tsx
create mode 100644 web/components/issues/issue-layouts/filters/header/helpers/index.ts
create mode 100644 web/components/issues/issue-layouts/filters/header/index.ts
create mode 100644 web/components/issues/issue-layouts/filters/header/layout-selection.tsx
create mode 100644 web/components/issues/issue-layouts/filters/index.ts
create mode 100644 web/components/issues/issue-layouts/gantt/base-gantt-root.tsx
rename web/components/issues/{gantt-chart => issue-layouts/gantt}/blocks.tsx (59%)
create mode 100644 web/components/issues/issue-layouts/gantt/cycle-root.tsx
create mode 100644 web/components/issues/issue-layouts/gantt/index.ts
create mode 100644 web/components/issues/issue-layouts/gantt/module-root.tsx
create mode 100644 web/components/issues/issue-layouts/gantt/project-root.tsx
create mode 100644 web/components/issues/issue-layouts/gantt/project-view-root.tsx
create mode 100644 web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx
create mode 100644 web/components/issues/issue-layouts/index.ts
create mode 100644 web/components/issues/issue-layouts/kanban/base-kanban-root.tsx
create mode 100644 web/components/issues/issue-layouts/kanban/block.tsx
create mode 100644 web/components/issues/issue-layouts/kanban/blocks-list.tsx
create mode 100644 web/components/issues/issue-layouts/kanban/default.tsx
create mode 100644 web/components/issues/issue-layouts/kanban/headers/assignee.tsx
create mode 100644 web/components/issues/issue-layouts/kanban/headers/created_by.tsx
create mode 100644 web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx
create mode 100644 web/components/issues/issue-layouts/kanban/headers/group-by-root.tsx
create mode 100644 web/components/issues/issue-layouts/kanban/headers/label.tsx
create mode 100644 web/components/issues/issue-layouts/kanban/headers/priority.tsx
create mode 100644 web/components/issues/issue-layouts/kanban/headers/project.tsx
create mode 100644 web/components/issues/issue-layouts/kanban/headers/state-group.tsx
create mode 100644 web/components/issues/issue-layouts/kanban/headers/state.tsx
create mode 100644 web/components/issues/issue-layouts/kanban/headers/sub-group-by-card.tsx
create mode 100644 web/components/issues/issue-layouts/kanban/headers/sub-group-by-root.tsx
create mode 100644 web/components/issues/issue-layouts/kanban/index.ts
create mode 100644 web/components/issues/issue-layouts/kanban/properties.tsx
create mode 100644 web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx
create mode 100644 web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx
create mode 100644 web/components/issues/issue-layouts/kanban/roots/draft-issue-root.tsx
create mode 100644 web/components/issues/issue-layouts/kanban/roots/index.ts
create mode 100644 web/components/issues/issue-layouts/kanban/roots/module-root.tsx
create mode 100644 web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx
create mode 100644 web/components/issues/issue-layouts/kanban/roots/project-root.tsx
create mode 100644 web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx
create mode 100644 web/components/issues/issue-layouts/kanban/swimlanes.tsx
create mode 100644 web/components/issues/issue-layouts/list/base-list-root.tsx
create mode 100644 web/components/issues/issue-layouts/list/block.tsx
create mode 100644 web/components/issues/issue-layouts/list/blocks-list.tsx
create mode 100644 web/components/issues/issue-layouts/list/default.tsx
create mode 100644 web/components/issues/issue-layouts/list/headers/assignee.tsx
create mode 100644 web/components/issues/issue-layouts/list/headers/created-by.tsx
create mode 100644 web/components/issues/issue-layouts/list/headers/empty-group.tsx
create mode 100644 web/components/issues/issue-layouts/list/headers/group-by-card.tsx
create mode 100644 web/components/issues/issue-layouts/list/headers/group-by-root.tsx
create mode 100644 web/components/issues/issue-layouts/list/headers/label.tsx
create mode 100644 web/components/issues/issue-layouts/list/headers/priority.tsx
create mode 100644 web/components/issues/issue-layouts/list/headers/project.tsx
create mode 100644 web/components/issues/issue-layouts/list/headers/state-group.tsx
create mode 100644 web/components/issues/issue-layouts/list/headers/state.tsx
create mode 100644 web/components/issues/issue-layouts/list/index.ts
create mode 100644 web/components/issues/issue-layouts/list/list-view-types.d.ts
create mode 100644 web/components/issues/issue-layouts/list/properties.tsx
create mode 100644 web/components/issues/issue-layouts/list/quick-add-issue-form.tsx
create mode 100644 web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx
create mode 100644 web/components/issues/issue-layouts/list/roots/cycle-root.tsx
create mode 100644 web/components/issues/issue-layouts/list/roots/draft-issue-root.tsx
create mode 100644 web/components/issues/issue-layouts/list/roots/index.ts
create mode 100644 web/components/issues/issue-layouts/list/roots/module-root.tsx
create mode 100644 web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx
create mode 100644 web/components/issues/issue-layouts/list/roots/project-root.tsx
create mode 100644 web/components/issues/issue-layouts/list/roots/project-view-root.tsx
create mode 100644 web/components/issues/issue-layouts/properties/assignee.tsx
create mode 100644 web/components/issues/issue-layouts/properties/date.tsx
create mode 100644 web/components/issues/issue-layouts/properties/estimates.tsx
create mode 100644 web/components/issues/issue-layouts/properties/index.tsx
create mode 100644 web/components/issues/issue-layouts/properties/labels.tsx
create mode 100644 web/components/issues/issue-layouts/properties/priority.tsx
create mode 100644 web/components/issues/issue-layouts/properties/state.tsx
create mode 100644 web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx
create mode 100644 web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx
create mode 100644 web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx
create mode 100644 web/components/issues/issue-layouts/quick-action-dropdowns/index.ts
create mode 100644 web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx
create mode 100644 web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx
create mode 100644 web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx
create mode 100644 web/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx
create mode 100644 web/components/issues/issue-layouts/roots/cycle-layout-root.tsx
create mode 100644 web/components/issues/issue-layouts/roots/draft-issue-layout-root.tsx
create mode 100644 web/components/issues/issue-layouts/roots/index.ts
create mode 100644 web/components/issues/issue-layouts/roots/module-layout-root.tsx
create mode 100644 web/components/issues/issue-layouts/roots/project-layout-root.tsx
create mode 100644 web/components/issues/issue-layouts/roots/project-view-layout-root.tsx
create mode 100644 web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx
create mode 100644 web/components/issues/issue-layouts/spreadsheet/columns/assignee-column.tsx
create mode 100644 web/components/issues/issue-layouts/spreadsheet/columns/attachment-column.tsx
create mode 100644 web/components/issues/issue-layouts/spreadsheet/columns/columns-list.tsx
create mode 100644 web/components/issues/issue-layouts/spreadsheet/columns/created-on-column.tsx
create mode 100644 web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx
create mode 100644 web/components/issues/issue-layouts/spreadsheet/columns/estimate-column.tsx
rename web/components/{core/views/spreadsheet-view => issues/issue-layouts/spreadsheet/columns}/index.ts (64%)
rename web/components/{core/views/spreadsheet-view/issue-column => issues/issue-layouts/spreadsheet/columns/issue}/index.ts (100%)
create mode 100644 web/components/issues/issue-layouts/spreadsheet/columns/issue/issue-column.tsx
rename web/components/{core/views/spreadsheet-view/issue-column => issues/issue-layouts/spreadsheet/columns/issue}/spreadsheet-issue-column.tsx (59%)
create mode 100644 web/components/issues/issue-layouts/spreadsheet/columns/label-column.tsx
create mode 100644 web/components/issues/issue-layouts/spreadsheet/columns/link-column.tsx
create mode 100644 web/components/issues/issue-layouts/spreadsheet/columns/priority-column.tsx
create mode 100644 web/components/issues/issue-layouts/spreadsheet/columns/start-date-column.tsx
create mode 100644 web/components/issues/issue-layouts/spreadsheet/columns/state-column.tsx
create mode 100644 web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx
create mode 100644 web/components/issues/issue-layouts/spreadsheet/columns/updated-on-column.tsx
create mode 100644 web/components/issues/issue-layouts/spreadsheet/index.ts
create mode 100644 web/components/issues/issue-layouts/spreadsheet/quick-add-issue-form.tsx
create mode 100644 web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx
create mode 100644 web/components/issues/issue-layouts/spreadsheet/roots/index.ts
create mode 100644 web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx
create mode 100644 web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx
create mode 100644 web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx
create mode 100644 web/components/issues/issue-layouts/spreadsheet/spreadsheet-column.tsx
create mode 100644 web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx
create mode 100644 web/components/issues/issue-layouts/types.ts
create mode 100644 web/components/issues/issue-peek-overview/activity/card.tsx
create mode 100644 web/components/issues/issue-peek-overview/activity/comment-card.tsx
create mode 100644 web/components/issues/issue-peek-overview/activity/comment-editor.tsx
create mode 100644 web/components/issues/issue-peek-overview/activity/comment-reaction.tsx
create mode 100644 web/components/issues/issue-peek-overview/activity/index.ts
create mode 100644 web/components/issues/issue-peek-overview/activity/view.tsx
create mode 100644 web/components/issues/issue-peek-overview/index.ts
create mode 100644 web/components/issues/issue-peek-overview/issue-detail.tsx
create mode 100644 web/components/issues/issue-peek-overview/properties.tsx
create mode 100644 web/components/issues/issue-peek-overview/reactions/index.ts
create mode 100644 web/components/issues/issue-peek-overview/reactions/preview.tsx
create mode 100644 web/components/issues/issue-peek-overview/reactions/root.tsx
create mode 100644 web/components/issues/issue-peek-overview/reactions/selector.tsx
create mode 100644 web/components/issues/issue-peek-overview/root.tsx
create mode 100644 web/components/issues/issue-peek-overview/view.tsx
delete mode 100644 web/components/issues/my-issues/index.ts
delete mode 100644 web/components/issues/my-issues/my-issues-select-filters.tsx
delete mode 100644 web/components/issues/my-issues/my-issues-view-options.tsx
delete mode 100644 web/components/issues/my-issues/my-issues-view.tsx
delete mode 100644 web/components/issues/peek-overview/full-screen-peek-view.tsx
delete mode 100644 web/components/issues/peek-overview/header.tsx
delete mode 100644 web/components/issues/peek-overview/index.ts
delete mode 100644 web/components/issues/peek-overview/issue-activity.tsx
delete mode 100644 web/components/issues/peek-overview/issue-details.tsx
delete mode 100644 web/components/issues/peek-overview/issue-properties.tsx
delete mode 100644 web/components/issues/peek-overview/layout.tsx
delete mode 100644 web/components/issues/peek-overview/side-peek-view.tsx
create mode 100644 web/components/issues/select/cycle.tsx
create mode 100644 web/components/issues/select/module.tsx
delete mode 100644 web/components/issues/view-select/assignee.tsx
delete mode 100644 web/components/issues/view-select/label.tsx
delete mode 100644 web/components/issues/view-select/priority.tsx
delete mode 100644 web/components/issues/workspace-views/workpace-view-issues.tsx
delete mode 100644 web/components/issues/workspace-views/workspace-all-issue.tsx
delete mode 100644 web/components/issues/workspace-views/workspace-assigned-issue.tsx
delete mode 100644 web/components/issues/workspace-views/workspace-created-issues.tsx
delete mode 100644 web/components/issues/workspace-views/workspace-issue-view-option.tsx
delete mode 100644 web/components/issues/workspace-views/workspace-subscribed-issue.tsx
create mode 100644 web/components/labels/label-block/drag-handle.tsx
create mode 100644 web/components/labels/label-block/label-item-block.tsx
create mode 100644 web/components/labels/label-block/label-name.tsx
create mode 100644 web/components/labels/label-select.tsx
create mode 100644 web/components/labels/project-setting-label-group.tsx
create mode 100644 web/components/labels/project-setting-label-item.tsx
create mode 100644 web/components/labels/project-setting-label-list.tsx
delete mode 100644 web/components/labels/single-label-group.tsx
delete mode 100644 web/components/labels/single-label.tsx
delete mode 100644 web/components/modules/gantt-chart/module-issues-layout.tsx
create mode 100644 web/components/modules/module-card-item.tsx
create mode 100644 web/components/modules/module-list-item.tsx
create mode 100644 web/components/modules/module-peek-overview.tsx
create mode 100644 web/components/modules/modules-list-view.tsx
delete mode 100644 web/components/modules/single-module-card.tsx
create mode 100644 web/components/onboarding/invitations.tsx
create mode 100644 web/components/onboarding/onboarding-sidebar.tsx
create mode 100644 web/components/onboarding/step-indicator.tsx
create mode 100644 web/components/onboarding/switch-delete-account-modal.tsx
create mode 100644 web/components/page-views/index.ts
create mode 100644 web/components/page-views/signin.tsx
create mode 100644 web/components/page-views/workspace-dashboard.tsx
create mode 100644 web/components/pages/pages-list/archived-pages-list.tsx
create mode 100644 web/components/pages/pages-list/list-item.tsx
create mode 100644 web/components/pages/pages-list/list-view.tsx
delete mode 100644 web/components/pages/pages-list/my-pages-list.tsx
delete mode 100644 web/components/pages/pages-list/other-pages-list.tsx
create mode 100644 web/components/pages/pages-list/private-page-list.tsx
create mode 100644 web/components/pages/pages-list/shared-pages-list.tsx
delete mode 100644 web/components/pages/pages-view.tsx
delete mode 100644 web/components/pages/single-page-block.tsx
delete mode 100644 web/components/pages/single-page-detailed-item.tsx
delete mode 100644 web/components/pages/single-page-list-item.tsx
create mode 100644 web/components/profile/profile-issues-filter.tsx
delete mode 100644 web/components/profile/profile-issues-view-options.tsx
delete mode 100644 web/components/profile/profile-issues-view.tsx
create mode 100644 web/components/profile/profile-issues.tsx
create mode 100644 web/components/project/card-list.tsx
create mode 100644 web/components/project/card.tsx
create mode 100644 web/components/project/empty-state.tsx
create mode 100644 web/components/project/form-loader.tsx
create mode 100644 web/components/project/form.tsx
rename web/components/project/{single-integration-card.tsx => integration-card.tsx} (87%)
delete mode 100644 web/components/project/label-select.tsx
rename web/components/project/{confirm-project-leave-modal.tsx => leave-project-modal.tsx} (63%)
create mode 100644 web/components/project/member-list-item.tsx
create mode 100644 web/components/project/member-list.tsx
create mode 100644 web/components/project/project-settings-member-defaults.tsx
create mode 100644 web/components/project/publish-project/index.tsx
delete mode 100644 web/components/project/settings-sidebar.tsx
create mode 100644 web/components/project/settings/delete-project-section.tsx
create mode 100644 web/components/project/settings/features-list.tsx
create mode 100644 web/components/project/settings/index.ts
delete mode 100644 web/components/project/settings/single-label.tsx
create mode 100644 web/components/project/sidebar-list-item.tsx
delete mode 100644 web/components/project/single-project-card.tsx
delete mode 100644 web/components/project/single-sidebar-project.tsx
delete mode 100644 web/components/search-listbox/index.tsx
delete mode 100644 web/components/search-listbox/types.d.ts
create mode 100644 web/components/states/project-setting-state-list-item.tsx
create mode 100644 web/components/states/project-setting-state-list.tsx
delete mode 100644 web/components/states/single-state.tsx
delete mode 100644 web/components/tiptap/bubble-menu/index.tsx
delete mode 100644 web/components/tiptap/bubble-menu/link-selector.tsx
delete mode 100644 web/components/tiptap/bubble-menu/node-selector.tsx
delete mode 100644 web/components/tiptap/bubble-menu/utils/link-validator.tsx
delete mode 100644 web/components/tiptap/extensions/index.tsx
delete mode 100644 web/components/tiptap/extensions/table/table-cell.ts
delete mode 100644 web/components/tiptap/extensions/table/table-header.ts
delete mode 100644 web/components/tiptap/extensions/table/table.ts
delete mode 100644 web/components/tiptap/extensions/updated-image.tsx
delete mode 100644 web/components/tiptap/index.tsx
delete mode 100644 web/components/tiptap/plugins/delete-image.tsx
delete mode 100644 web/components/tiptap/plugins/upload-image.tsx
delete mode 100644 web/components/tiptap/slash-command/index.tsx
delete mode 100644 web/components/tiptap/table-menu/InsertBottomTableIcon.tsx
delete mode 100644 web/components/tiptap/table-menu/InsertLeftTableIcon.tsx
delete mode 100644 web/components/tiptap/table-menu/InsertRightTableIcon.tsx
delete mode 100644 web/components/tiptap/table-menu/InsertTopTableIcon.tsx
delete mode 100644 web/components/tiptap/table-menu/index.tsx
delete mode 100644 web/components/tiptap/utils.ts
delete mode 100644 web/components/ui/avatar.tsx
delete mode 100644 web/components/ui/buttons/danger-button.tsx
delete mode 100644 web/components/ui/buttons/index.ts
delete mode 100644 web/components/ui/buttons/primary-button.tsx
delete mode 100644 web/components/ui/buttons/secondary-button.tsx
delete mode 100644 web/components/ui/buttons/type.d.ts
delete mode 100644 web/components/ui/circular-progress.tsx
delete mode 100644 web/components/ui/dropdowns/context-menu.tsx
delete mode 100644 web/components/ui/dropdowns/custom-menu.tsx
delete mode 100644 web/components/ui/dropdowns/custom-search-select.tsx
delete mode 100644 web/components/ui/dropdowns/custom-select.tsx
delete mode 100644 web/components/ui/dropdowns/types.d.ts
delete mode 100644 web/components/ui/icon-name-type.d.ts
delete mode 100644 web/components/ui/input/index.tsx
delete mode 100644 web/components/ui/input/types.d.ts
delete mode 100644 web/components/ui/text-area/index.tsx
delete mode 100644 web/components/ui/text-area/types.d.ts
create mode 100644 web/components/user/index.ts
create mode 100644 web/components/user/user-greetings.tsx
delete mode 100644 web/components/views/gantt-chart.tsx
delete mode 100644 web/components/views/select-filters.tsx
delete mode 100644 web/components/views/single-view-item.tsx
create mode 100644 web/components/views/view-list-item.tsx
create mode 100644 web/components/views/views-list.tsx
create mode 100644 web/components/web-hooks/delete-webhook-modal.tsx
create mode 100644 web/components/web-hooks/empty-state.tsx
create mode 100644 web/components/web-hooks/form/delete-section.tsx
create mode 100644 web/components/web-hooks/form/event-types.tsx
create mode 100644 web/components/web-hooks/form/form.tsx
create mode 100644 web/components/web-hooks/form/index.ts
create mode 100644 web/components/web-hooks/form/individual-event-options.tsx
create mode 100644 web/components/web-hooks/form/input.tsx
create mode 100644 web/components/web-hooks/form/secret-key.tsx
create mode 100644 web/components/web-hooks/form/toggle.tsx
create mode 100644 web/components/web-hooks/index.ts
create mode 100644 web/components/web-hooks/utils.ts
create mode 100644 web/components/web-hooks/webhooks-list-item.tsx
create mode 100644 web/components/web-hooks/webhooks-list.tsx
delete mode 100644 web/components/web-view/activity-message.tsx
delete mode 100644 web/components/web-view/add-comment.tsx
delete mode 100644 web/components/web-view/create-update-link-form.tsx
delete mode 100644 web/components/web-view/index.ts
delete mode 100644 web/components/web-view/issue-activity.tsx
delete mode 100644 web/components/web-view/issue-attachments.tsx
delete mode 100644 web/components/web-view/issue-link-list.tsx
delete mode 100644 web/components/web-view/issue-properties-detail.tsx
delete mode 100644 web/components/web-view/issue-web-view-form.tsx
delete mode 100644 web/components/web-view/label.tsx
delete mode 100644 web/components/web-view/select-assignee.tsx
delete mode 100644 web/components/web-view/select-blocked.tsx
delete mode 100644 web/components/web-view/select-blocker.tsx
delete mode 100644 web/components/web-view/select-estimate.tsx
delete mode 100644 web/components/web-view/select-parent.tsx
delete mode 100644 web/components/web-view/select-priority.tsx
delete mode 100644 web/components/web-view/select-state.tsx
delete mode 100644 web/components/web-view/sub-issues.tsx
delete mode 100644 web/components/web-view/web-view-modal.tsx
create mode 100644 web/components/workspace/member-select.tsx
create mode 100644 web/components/workspace/settings/index.ts
create mode 100644 web/components/workspace/settings/members-list-item.tsx
create mode 100644 web/components/workspace/settings/members-list.tsx
create mode 100644 web/components/workspace/settings/workspace-details.tsx
create mode 100644 web/components/workspace/views/default-view-list-item.tsx
rename web/components/workspace/views/{delete-workspace-view-modal.tsx => delete-view-modal.tsx} (58%)
delete mode 100644 web/components/workspace/views/global-select-filters.tsx
create mode 100644 web/components/workspace/views/header.tsx
create mode 100644 web/components/workspace/views/index.ts
delete mode 100644 web/components/workspace/views/single-workspace-view-item.tsx
create mode 100644 web/components/workspace/views/view-list-item.tsx
create mode 100644 web/components/workspace/views/views-list.tsx
delete mode 100644 web/components/workspace/views/workpace-view-navigation.tsx
create mode 100644 web/constants/common.ts
delete mode 100644 web/constants/crisp.tsx
create mode 100644 web/constants/cycle.ts
create mode 100644 web/constants/kanban-helpers.ts
create mode 100644 web/constants/page.ts
delete mode 100644 web/contexts/inbox-view-context.tsx
delete mode 100644 web/contexts/project-member.context.tsx
delete mode 100644 web/contexts/theme.context.tsx
delete mode 100644 web/contexts/workspace-view-context.tsx
delete mode 100644 web/contexts/workspace.context.tsx
create mode 100644 web/helpers/download.helper.ts
create mode 100644 web/helpers/event-tracker.helper.ts
create mode 100644 web/helpers/filter.helper.ts
create mode 100644 web/helpers/generate-random-string.ts
create mode 100644 web/helpers/issue.helper.ts
create mode 100644 web/helpers/user.helper.ts
delete mode 100644 web/hooks/gantt-chart/cycle-issues-view.tsx
delete mode 100644 web/hooks/gantt-chart/issue-view.tsx
delete mode 100644 web/hooks/gantt-chart/module-issues-view.tsx
delete mode 100644 web/hooks/gantt-chart/view-issues-view.tsx
delete mode 100644 web/hooks/my-issues/use-my-issues-filter.tsx
delete mode 100644 web/hooks/my-issues/use-my-issues.tsx
delete mode 100644 web/hooks/use-calendar-issues-view.tsx
create mode 100644 web/hooks/use-draggable-portal.ts
create mode 100644 web/hooks/use-editor-suggestions.tsx
delete mode 100644 web/hooks/use-inbox-view.tsx
delete mode 100644 web/hooks/use-issue-properties.tsx
delete mode 100644 web/hooks/use-issues-view.tsx
delete mode 100644 web/hooks/use-profile-issues.tsx
delete mode 100644 web/hooks/use-project-members.tsx
delete mode 100644 web/hooks/use-projects.tsx
delete mode 100644 web/hooks/use-spreadsheet-issues-view.tsx
delete mode 100644 web/hooks/use-theme.tsx
delete mode 100644 web/hooks/use-workspace-details.tsx
delete mode 100644 web/hooks/use-workspace-members.tsx
delete mode 100644 web/hooks/use-workspace-view.tsx
delete mode 100644 web/hooks/use-workspaces.tsx
create mode 100644 web/layouts/admin-layout/header.tsx
create mode 100644 web/layouts/admin-layout/index.ts
create mode 100644 web/layouts/admin-layout/layout.tsx
create mode 100644 web/layouts/admin-layout/sidebar.tsx
delete mode 100644 web/layouts/app-layout/app-header.tsx
delete mode 100644 web/layouts/app-layout/app-sidebar.tsx
create mode 100644 web/layouts/app-layout/index.ts
create mode 100644 web/layouts/app-layout/layout.tsx
create mode 100644 web/layouts/app-layout/sidebar.tsx
create mode 100644 web/layouts/auth-layout/admin-wrapper.tsx
delete mode 100644 web/layouts/auth-layout/project-authorization-wrapper.tsx
create mode 100644 web/layouts/auth-layout/project-wrapper.tsx
delete mode 100644 web/layouts/auth-layout/user-authorization-wrapper.tsx
create mode 100644 web/layouts/auth-layout/user-wrapper.tsx
delete mode 100644 web/layouts/auth-layout/workspace-authorization-wrapper.tsx
create mode 100644 web/layouts/auth-layout/workspace-wrapper.tsx
create mode 100644 web/layouts/instance-layout/index.tsx
delete mode 100644 web/layouts/profile-layout.tsx
create mode 100644 web/layouts/settings-layout/index.ts
create mode 100644 web/layouts/settings-layout/profile/index.ts
create mode 100644 web/layouts/settings-layout/profile/layout.tsx
create mode 100644 web/layouts/settings-layout/profile/sidebar.tsx
create mode 100644 web/layouts/settings-layout/project/index.ts
create mode 100644 web/layouts/settings-layout/project/layout.tsx
create mode 100644 web/layouts/settings-layout/project/sidebar.tsx
create mode 100644 web/layouts/settings-layout/workspace/index.ts
create mode 100644 web/layouts/settings-layout/workspace/layout.tsx
create mode 100644 web/layouts/settings-layout/workspace/sidebar.tsx
rename web/{components/issues/gantt-chart => layouts/user-profile-layout}/index.ts (50%)
create mode 100644 web/layouts/user-profile-layout/layout.tsx
delete mode 100644 web/layouts/web-view-layout/index.tsx
create mode 100644 web/lib/app-provider.tsx
delete mode 100644 web/lib/auth.ts
delete mode 100644 web/lib/cookie.ts
create mode 100644 web/lib/local-storage.ts
delete mode 100644 web/lib/mobx/store-init.tsx
create mode 100644 web/lib/wrappers/crisp-wrapper.tsx
create mode 100644 web/lib/wrappers/posthog-wrapper.tsx
create mode 100644 web/lib/wrappers/store-wrapper.tsx
delete mode 100644 web/pages/[workspaceSlug]/editor.tsx
delete mode 100644 web/pages/[workspaceSlug]/me/my-issues.tsx
delete mode 100644 web/pages/[workspaceSlug]/me/profile/activity.tsx
delete mode 100644 web/pages/[workspaceSlug]/me/profile/index.tsx
delete mode 100644 web/pages/[workspaceSlug]/me/profile/preferences.tsx
create mode 100644 web/pages/[workspaceSlug]/settings/api-tokens.tsx
create mode 100644 web/pages/[workspaceSlug]/settings/webhooks/[webhookId].tsx
create mode 100644 web/pages/[workspaceSlug]/settings/webhooks/create.tsx
create mode 100644 web/pages/[workspaceSlug]/settings/webhooks/index.tsx
create mode 100644 web/pages/[workspaceSlug]/workspace-views/[globalViewId].tsx
delete mode 100644 web/pages/[workspaceSlug]/workspace-views/issues.tsx
create mode 100644 web/pages/accounts/password.tsx
rename web/pages/{ => accounts}/sign-up.tsx (58%)
delete mode 100644 web/pages/api/slack-redirect.ts
delete mode 100644 web/pages/api/track-event.ts
delete mode 100644 web/pages/api/unsplash.ts
delete mode 100644 web/pages/error.tsx
create mode 100644 web/pages/god-mode/ai.tsx
create mode 100644 web/pages/god-mode/authorization.tsx
create mode 100644 web/pages/god-mode/email.tsx
create mode 100644 web/pages/god-mode/image.tsx
create mode 100644 web/pages/god-mode/index.tsx
delete mode 100644 web/pages/invitations.tsx
create mode 100644 web/pages/invitations/index.tsx
delete mode 100644 web/pages/m/[workspaceSlug]/editor.tsx
delete mode 100644 web/pages/m/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx
delete mode 100644 web/pages/magic-sign-in.tsx
delete mode 100644 web/pages/onboarding.tsx
create mode 100644 web/pages/onboarding/index.tsx
create mode 100644 web/pages/profile/activity.tsx
create mode 100644 web/pages/profile/change-password.tsx
create mode 100644 web/pages/profile/index.tsx
create mode 100644 web/pages/profile/preferences.tsx
delete mode 100644 web/pages/reset-password.tsx
create mode 100644 web/pages/workspace-invitations/index.tsx
delete mode 100644 web/pages/workspace-member-invitation.tsx
create mode 100644 web/public/auth/access-denied.svg
create mode 100644 web/public/emoji/project-emoji.svg
create mode 100644 web/public/empty-state/Project_full_screen.svg
create mode 100644 web/public/empty-state/api-token.svg
create mode 100644 web/public/empty-state/dashboard_empty_project.webp
create mode 100644 web/public/empty-state/empty_analytics.webp
create mode 100644 web/public/empty-state/empty_cycles.webp
create mode 100644 web/public/empty-state/empty_issues.webp
create mode 100644 web/public/empty-state/empty_label.svg
create mode 100644 web/public/empty-state/empty_members.svg
create mode 100644 web/public/empty-state/empty_modules.webp
create mode 100644 web/public/empty-state/empty_page.webp
create mode 100644 web/public/empty-state/empty_project.webp
create mode 100644 web/public/empty-state/empty_view.webp
create mode 100644 web/public/empty-state/web-hook.svg
create mode 100644 web/public/instance-not-ready.svg
create mode 100644 web/public/instance-setup-done.svg
create mode 100644 web/public/instance/plane-instance-not-ready.webp
create mode 100644 web/public/logos/github-dark.svg
create mode 100644 web/public/onboarding/onboarding-issues.svg
create mode 100644 web/public/onboarding/onboarding-pages.svg
create mode 100644 web/public/onboarding/sign-in.svg
create mode 100644 web/public/onboarding/user-dark.svg
create mode 100644 web/public/onboarding/user-light.svg
delete mode 100644 web/public/services/jira.png
create mode 100644 web/public/services/jira.svg
create mode 100644 web/public/users/user-1.png
create mode 100644 web/public/users/user-2.png
create mode 100644 web/public/web-view-spinner.png
create mode 100644 web/services/api_token.service.ts
rename web/services/{app-config.service.ts => app_config.service.ts} (55%)
rename web/services/{app-installations.service.ts => app_installation.service.ts} (81%)
create mode 100644 web/services/auth.service.ts
delete mode 100644 web/services/authentication.service.ts
create mode 100644 web/services/cycle.service.ts
delete mode 100644 web/services/cycles.service.ts
delete mode 100644 web/services/estimates.service.ts
create mode 100644 web/services/instance.service.ts
delete mode 100644 web/services/integration/csv.services.ts
delete mode 100644 web/services/integration/github.service.ts
delete mode 100644 web/services/integration/jira.service.ts
create mode 100644 web/services/integrations/github.service.ts
create mode 100644 web/services/integrations/index.ts
rename web/services/{integration/index.ts => integrations/integration.service.ts} (65%)
create mode 100644 web/services/integrations/jira.service.ts
create mode 100644 web/services/issue/index.ts
create mode 100644 web/services/issue/issue.service.ts
create mode 100644 web/services/issue/issue_archive.service.ts
create mode 100644 web/services/issue/issue_attachment.service.ts
create mode 100644 web/services/issue/issue_comment.service.ts
create mode 100644 web/services/issue/issue_draft.service.tsx
create mode 100644 web/services/issue/issue_label.service.ts
create mode 100644 web/services/issue/issue_reaction.service.ts
delete mode 100644 web/services/issues.service.ts
create mode 100644 web/services/module.service.ts
delete mode 100644 web/services/modules.service.ts
rename web/services/{notifications.service.ts => notification.service.ts} (59%)
rename web/services/{pages.service.ts => page.service.ts} (50%)
delete mode 100644 web/services/project-publish.service.ts
delete mode 100644 web/services/project.service.ts
create mode 100644 web/services/project/index.ts
create mode 100644 web/services/project/project-estimate.service.ts
create mode 100644 web/services/project/project-export.service.ts
create mode 100644 web/services/project/project-member.service.ts
create mode 100644 web/services/project/project-publish.service.ts
create mode 100644 web/services/project/project-state.service.ts
create mode 100644 web/services/project/project.service.ts
delete mode 100644 web/services/reaction.service.ts
delete mode 100644 web/services/state.service.ts
delete mode 100644 web/services/track-event.service.ts
create mode 100644 web/services/view.service.ts
delete mode 100644 web/services/views.service.ts
delete mode 100644 web/services/web-waitlist.service.ts
create mode 100644 web/services/webhook.service.ts
create mode 100644 web/store/app-config.store.ts
create mode 100644 web/store/archived-issues/index.ts
create mode 100644 web/store/archived-issues/issue.store.ts
create mode 100644 web/store/archived-issues/issue_detail.store.ts
create mode 100644 web/store/archived-issues/issue_filters.store.ts
create mode 100644 web/store/calendar.store.ts
create mode 100644 web/store/command-palette.store.ts
create mode 100644 web/store/cycle-issues/index.ts
create mode 100644 web/store/cycle-issues/issue_filters.store.ts
create mode 100644 web/store/cycle/cycle_issue.store.ts
create mode 100644 web/store/cycle/cycle_issue_calendar_view.store.ts
create mode 100644 web/store/cycle/cycle_issue_filters.store.ts
create mode 100644 web/store/cycle/cycle_issue_kanban_view.store.ts
create mode 100644 web/store/cycle/cycles.store.ts
create mode 100644 web/store/cycle/index.ts
create mode 100644 web/store/draft-issues/index.ts
create mode 100644 web/store/draft-issues/issue.store.ts
create mode 100644 web/store/draft-issues/issue_filters.store.ts
create mode 100644 web/store/editor/index.ts
create mode 100644 web/store/editor/mentions.store.ts
create mode 100644 web/store/event-tracker.store.ts
create mode 100644 web/store/global-view/global_view_filters.store.ts
create mode 100644 web/store/global-view/global_view_issues.store.ts
create mode 100644 web/store/global-view/global_views.store.ts
create mode 100644 web/store/global-view/index.ts
create mode 100644 web/store/inbox/inbox.store.ts
create mode 100644 web/store/inbox/inbox_filters.store.ts
create mode 100644 web/store/inbox/inbox_issue_detail.store.ts
create mode 100644 web/store/inbox/inbox_issues.store.ts
create mode 100644 web/store/inbox/index.ts
create mode 100644 web/store/instance/index.ts
create mode 100644 web/store/instance/instance.store.ts
create mode 100644 web/store/issue/index.ts
create mode 100644 web/store/issue/issue.store.ts
create mode 100644 web/store/issue/issue_calendar_view.store.ts
create mode 100644 web/store/issue/issue_detail.store.ts
rename web/store/{draft-issue.ts => issue/issue_draft.store.ts} (74%)
create mode 100644 web/store/issue/issue_filters.store.ts
create mode 100644 web/store/issue/issue_kanban_view.store.ts
create mode 100644 web/store/issue/issue_quick_add.store.ts
delete mode 100644 web/store/issues.ts
create mode 100644 web/store/issues/base-issue-calendar-helper.store.ts
create mode 100644 web/store/issues/base-issue-kanban-helper.store.ts
create mode 100644 web/store/issues/global/filter.store.ts
create mode 100644 web/store/issues/global/issue.store.ts
create mode 100644 web/store/issues/index.ts
create mode 100644 web/store/issues/profile/filter.store.ts
create mode 100644 web/store/issues/profile/issue.store.ts
create mode 100644 web/store/issues/project-issues/archived/filter.store.ts
create mode 100644 web/store/issues/project-issues/archived/issue.store.ts
create mode 100644 web/store/issues/project-issues/base-issue-filter.store.ts
create mode 100644 web/store/issues/project-issues/base-issue.store.ts
create mode 100644 web/store/issues/project-issues/cycle/filter.store.ts
create mode 100644 web/store/issues/project-issues/cycle/issue.store.ts
create mode 100644 web/store/issues/project-issues/draft/filter.store.ts
create mode 100644 web/store/issues/project-issues/draft/issue.store.ts
create mode 100644 web/store/issues/project-issues/issue-filters.store.ts
create mode 100644 web/store/issues/project-issues/module/filter.store.ts
create mode 100644 web/store/issues/project-issues/module/issue.store.ts
create mode 100644 web/store/issues/project-issues/project-view/filter.store.ts
create mode 100644 web/store/issues/project-issues/project-view/issue.store.ts
create mode 100644 web/store/issues/project-issues/project/filter.store.ts
create mode 100644 web/store/issues/project-issues/project/issue.store.ts
create mode 100644 web/store/issues/project-issues/utils.ts
create mode 100644 web/store/issues/types.ts
create mode 100644 web/store/module-issues/index.ts
create mode 100644 web/store/module-issues/issue_filters.store.ts
create mode 100644 web/store/module/index.ts
create mode 100644 web/store/module/module_filters.store.ts
create mode 100644 web/store/module/module_issue.store.ts
create mode 100644 web/store/module/module_issue_calendar_view.store.ts
create mode 100644 web/store/module/module_issue_kanban_view.store.ts
create mode 100644 web/store/module/modules.store.ts
create mode 100644 web/store/page.store.ts
create mode 100644 web/store/profile-issues/index.ts
create mode 100644 web/store/profile-issues/issue.store.ts
create mode 100644 web/store/profile-issues/issue_filters.store.ts
create mode 100644 web/store/project-view/index.ts
create mode 100644 web/store/project-view/project_view_filters.store.ts
create mode 100644 web/store/project-view/project_view_issue_calendar_view.store.ts
create mode 100644 web/store/project-view/project_view_issues.store.ts
create mode 100644 web/store/project-view/project_views.store.ts
delete mode 100644 web/store/project.ts
create mode 100644 web/store/project/index.ts
create mode 100644 web/store/project/project-estimates.store.ts
create mode 100644 web/store/project/project-label.store.ts
create mode 100644 web/store/project/project-members.store.ts
rename web/store/{project-publish.tsx => project/project-publish.store.ts} (55%)
create mode 100644 web/store/project/project-state.store.ts
create mode 100644 web/store/project/project.store.ts
rename web/store/{theme.ts => theme.store.ts} (62%)
create mode 100644 web/store/user.store.ts
delete mode 100644 web/store/user.ts
create mode 100644 web/store/webhook.store.ts
create mode 100644 web/store/workspace/index.ts
create mode 100644 web/store/workspace/workspace-member.store.ts
create mode 100644 web/store/workspace/workspace.store.ts
create mode 100644 web/store/workspace/workspace_filters.store.ts
create mode 100644 web/styles/table.css
create mode 100644 web/types/api_token.d.ts
create mode 100644 web/types/app.d.ts
create mode 100644 web/types/auth.d.ts
create mode 100644 web/types/instance.d.ts
create mode 100644 web/types/webhook.d.ts
diff --git a/.deepsource.toml b/.deepsource.toml
new file mode 100644
index 0000000000..2b40af672d
--- /dev/null
+++ b/.deepsource.toml
@@ -0,0 +1,23 @@
+version = 1
+
+exclude_patterns = [
+ "bin/**",
+ "**/node_modules/",
+ "**/*.min.js"
+]
+
+[[analyzers]]
+name = "shell"
+
+[[analyzers]]
+name = "javascript"
+
+ [analyzers.meta]
+ plugins = ["react"]
+ environment = ["nodejs"]
+
+[[analyzers]]
+name = "python"
+
+ [analyzers.meta]
+ runtime_version = "3.x.x"
\ No newline at end of file
diff --git a/.env.example b/.env.example
index 082aa753b8..90070de198 100644
--- a/.env.example
+++ b/.env.example
@@ -21,15 +21,15 @@ AWS_S3_BUCKET_NAME="uploads"
FILE_SIZE_LIMIT=5242880
# GPT settings
-OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint
-OPENAI_API_KEY="sk-" # add your openai key here
-GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access
+OPENAI_API_BASE="https://api.openai.com/v1" # deprecated
+OPENAI_API_KEY="sk-" # deprecated
+GPT_ENGINE="gpt-3.5-turbo" # deprecated
# Settings related to Docker
-DOCKERIZED=1
+DOCKERIZED=1 # deprecated
+
# set to 1 If using the pre-configured minio setup
USE_MINIO=1
# Nginx Configuration
NGINX_PORT=80
-
diff --git a/.github/workflows/build-branch.yml b/.github/workflows/build-branch.yml
new file mode 100644
index 0000000000..47bbb94c03
--- /dev/null
+++ b/.github/workflows/build-branch.yml
@@ -0,0 +1,227 @@
+name: Branch Build
+
+on:
+ pull_request:
+ types:
+ - closed
+ branches:
+ - master
+ - release
+ - preview
+ - qa
+ - develop
+
+env:
+ TARGET_BRANCH: ${{ github.event.pull_request.base.ref }}
+
+jobs:
+ branch_build_setup:
+ if: ${{ (github.event_name == 'pull_request' && github.event.action =='closed' && github.event.pull_request.merged == true) }}
+ name: Build-Push Web/Space/API/Proxy Docker Image
+ runs-on: ubuntu-20.04
+
+ steps:
+ - name: Check out the repo
+ uses: actions/checkout@v3.3.0
+
+ - name: Uploading Proxy Source
+ uses: actions/upload-artifact@v3
+ with:
+ name: proxy-src-code
+ path: ./nginx
+ - name: Uploading Backend Source
+ uses: actions/upload-artifact@v3
+ with:
+ name: backend-src-code
+ path: ./apiserver
+ - name: Uploading Web Source
+ uses: actions/upload-artifact@v3
+ with:
+ name: web-src-code
+ path: |
+ ./
+ !./apiserver
+ !./nginx
+ !./deploy
+ !./space
+ - name: Uploading Space Source
+ uses: actions/upload-artifact@v3
+ with:
+ name: space-src-code
+ path: |
+ ./
+ !./apiserver
+ !./nginx
+ !./deploy
+ !./web
+ outputs:
+ gh_branch_name: ${{ env.TARGET_BRANCH }}
+
+ branch_build_push_frontend:
+ runs-on: ubuntu-20.04
+ needs: [branch_build_setup]
+ env:
+ FRONTEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend-private:${{ needs.branch_build_setup.outputs.gh_branch_name }}
+ steps:
+ - name: Set Frontend Docker Tag
+ run: |
+ if [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
+ TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend-private:latest
+ elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "release" ] || [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "preview" ]; then
+ TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend-private:preview,${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:preview
+ else
+ TAG=${{ env.FRONTEND_TAG }}
+ fi
+ echo "FRONTEND_TAG=${TAG}" >> $GITHUB_ENV
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v2.5.0
+
+ - name: Login to Docker Hub
+ uses: docker/login-action@v2.1.0
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+ - name: Downloading Web Source Code
+ uses: actions/download-artifact@v3
+ with:
+ name: web-src-code
+
+ - name: Build and Push Frontend to Docker Container Registry
+ uses: docker/build-push-action@v4.0.0
+ with:
+ context: .
+ file: ./web/Dockerfile.web
+ platforms: linux/amd64
+ tags: ${{ env.FRONTEND_TAG }}
+ push: true
+ env:
+ DOCKER_BUILDKIT: 1
+ DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
+ DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ branch_build_push_space:
+ runs-on: ubuntu-20.04
+ needs: [branch_build_setup]
+ env:
+ SPACE_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-space-private:${{ needs.branch_build_setup.outputs.gh_branch_name }}
+ steps:
+ - name: Set Space Docker Tag
+ run: |
+ if [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
+ TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space:latest
+ elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "release" ] || [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "preview" ]; then
+ TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space-private:preview,${{ secrets.DOCKERHUB_USERNAME }}/plane-space:preview
+ else
+ TAG=${{ env.SPACE_TAG }}
+ fi
+ echo "SPACE_TAG=${TAG}" >> $GITHUB_ENV
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v2.5.0
+
+ - name: Login to Docker Hub
+ uses: docker/login-action@v2.1.0
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+ - name: Downloading Space Source Code
+ uses: actions/download-artifact@v3
+ with:
+ name: space-src-code
+
+ - name: Build and Push Space to Docker Hub
+ uses: docker/build-push-action@v4.0.0
+ with:
+ context: .
+ file: ./space/Dockerfile.space
+ platforms: linux/amd64
+ tags: ${{ env.SPACE_TAG }}
+ push: true
+ env:
+ DOCKER_BUILDKIT: 1
+ DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
+ DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ branch_build_push_backend:
+ runs-on: ubuntu-20.04
+ needs: [branch_build_setup]
+ env:
+ BACKEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend-private:${{ needs.branch_build_setup.outputs.gh_branch_name }}
+ steps:
+ - name: Set Backend Docker Tag
+ run: |
+ if [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
+ TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend-private:latest
+ elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "release" ] || [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "preview" ]; then
+ TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend-private:preview,${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:preview
+ else
+ TAG=${{ env.BACKEND_TAG }}
+ fi
+ echo "BACKEND_TAG=${TAG}" >> $GITHUB_ENV
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v2.5.0
+
+ - name: Login to Docker Hub
+ uses: docker/login-action@v2.1.0
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+ - name: Downloading Backend Source Code
+ uses: actions/download-artifact@v3
+ with:
+ name: backend-src-code
+
+ - name: Build and Push Backend to Docker Hub
+ uses: docker/build-push-action@v4.0.0
+ with:
+ context: .
+ file: ./Dockerfile.api
+ platforms: linux/amd64
+ push: true
+ tags: ${{ env.BACKEND_TAG }}
+ env:
+ DOCKER_BUILDKIT: 1
+ DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
+ DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ branch_build_push_proxy:
+ runs-on: ubuntu-20.04
+ needs: [branch_build_setup]
+ env:
+ PROXY_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy-private:${{ needs.branch_build_setup.outputs.gh_branch_name }}
+ steps:
+ - name: Set Proxy Docker Tag
+ run: |
+ if [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
+ TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy-private:latest
+ elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "release" ] || [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "preview" ]; then
+ TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy-private:preview,${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:preview
+ else
+ TAG=${{ env.PROXY_TAG }}
+ fi
+ echo "PROXY_TAG=${TAG}" >> $GITHUB_ENV
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v2.5.0
+
+ - name: Login to Docker Hub
+ uses: docker/login-action@v2.1.0
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - name: Downloading Proxy Source Code
+ uses: actions/download-artifact@v3
+ with:
+ name: proxy-src-code
+
+ - name: Build and Push Plane-Proxy to Docker Hub
+ uses: docker/build-push-action@v4.0.0
+ with:
+ context: .
+ file: ./Dockerfile
+ platforms: linux/amd64
+ tags: ${{ env.PROXY_TAG }}
+ push: true
+ env:
+ DOCKER_BUILDKIT: 1
+ DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
+ DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
diff --git a/.github/workflows/build-test-pull-request.yml b/.github/workflows/build-test-pull-request.yml
index 6dc7ae1e5e..c74975f48e 100644
--- a/.github/workflows/build-test-pull-request.yml
+++ b/.github/workflows/build-test-pull-request.yml
@@ -36,15 +36,13 @@ jobs:
- name: Build Plane's Main App
if: steps.changed-files.outputs.web_any_changed == 'true'
run: |
- cd web
yarn
- yarn build
+ yarn build --filter=web
- name: Build Plane's Deploy App
if: steps.changed-files.outputs.deploy_any_changed == 'true'
run: |
- cd space
yarn
- yarn build
+ yarn build --filter=space
diff --git a/.github/workflows/create-sync-pr.yml b/.github/workflows/create-sync-pr.yml
index 28e47a0d66..c8e27f3221 100644
--- a/.github/workflows/create-sync-pr.yml
+++ b/.github/workflows/create-sync-pr.yml
@@ -2,6 +2,8 @@ name: Create PR in Plane EE Repository to sync the changes
on:
pull_request:
+ branches:
+ - master
types:
- closed
diff --git a/.gitignore b/.gitignore
index 1e99e102ad..0b655bd0e7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,8 @@ node_modules
# Production
/build
+dist/
+out/
# Misc
.DS_Store
@@ -73,3 +75,8 @@ pnpm-lock.yaml
pnpm-workspace.yaml
.npmrc
+.secrets
+tmp/
+## packages
+dist
+.temp/
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index cd74b61213..9fa847b6e7 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -60,7 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
-hello@plane.so.
+squawk@plane.so.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b25a791d08..73d69fb2d5 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -8,8 +8,8 @@ Before submitting a new issue, please search the [issues](https://github.com/mak
While we want to fix all the [issues](https://github.com/makeplane/plane/issues), before fixing a bug we need to be able to reproduce and confirm it. Please provide us with a minimal reproduction scenario using a repository or [Gist](https://gist.github.com/). Having a live, reproducible scenario gives us the information without asking questions back & forth with additional questions like:
-- 3rd-party libraries being used and their versions
-- a use-case that fails
+- 3rd-party libraries being used and their versions
+- a use-case that fails
Without said minimal reproduction, we won't be able to investigate all [issues](https://github.com/makeplane/plane/issues), and the issue might not be resolved.
@@ -19,10 +19,10 @@ You can open a new issue with this [issue form](https://github.com/makeplane/pla
### Requirements
-- Node.js version v16.18.0
-- Python version 3.8+
-- Postgres version v14
-- Redis version v6.2.7
+- Node.js version v16.18.0
+- Python version 3.8+
+- Postgres version v14
+- Redis version v6.2.7
### Setup the project
@@ -81,8 +81,8 @@ If you would like to _implement_ it, an issue with your proposal must be submitt
To ensure consistency throughout the source code, please keep these rules in mind as you are working:
-- All features or bug fixes must be tested by one or more specs (unit-tests).
-- We use [Eslint default rule guide](https://eslint.org/docs/rules/), with minor changes. An automated formatter is available using prettier.
+- All features or bug fixes must be tested by one or more specs (unit-tests).
+- We use [Eslint default rule guide](https://eslint.org/docs/rules/), with minor changes. An automated formatter is available using prettier.
## Need help? Questions and suggestions
@@ -90,11 +90,11 @@ Questions, suggestions, and thoughts are most welcome. We can also be reached in
## Ways to contribute
-- Try Plane Cloud and the self hosting platform and give feedback
-- Add new integrations
-- Help with open [issues](https://github.com/makeplane/plane/issues) or [create your own](https://github.com/makeplane/plane/issues/new/choose)
-- Share your thoughts and suggestions with us
-- Help create tutorials and blog posts
-- Request a feature by submitting a proposal
-- Report a bug
-- **Improve documentation** - fix incomplete or missing [docs](https://docs.plane.so/), bad wording, examples or explanations.
+- Try Plane Cloud and the self hosting platform and give feedback
+- Add new integrations
+- Help with open [issues](https://github.com/makeplane/plane/issues) or [create your own](https://github.com/makeplane/plane/issues/new/choose)
+- Share your thoughts and suggestions with us
+- Help create tutorials and blog posts
+- Request a feature by submitting a proposal
+- Report a bug
+- **Improve documentation** - fix incomplete or missing [docs](https://docs.plane.so/), bad wording, examples or explanations.
diff --git a/Dockerfile b/Dockerfile
index 388c5a4ef9..0e5d2f1180 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -43,8 +43,6 @@ FROM python:3.11.1-alpine3.17 AS backend
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
-ENV DJANGO_SETTINGS_MODULE plane.settings.production
-ENV DOCKERIZED 1
WORKDIR /code
diff --git a/ENV_SETUP.md b/ENV_SETUP.md
new file mode 100644
index 0000000000..3e03244c66
--- /dev/null
+++ b/ENV_SETUP.md
@@ -0,0 +1,145 @@
+# Environment Variables
+
+
+Environment variables are distributed in various files. Please refer them carefully.
+
+## {PROJECT_FOLDER}/.env
+
+File is available in the project root folder
+
+```
+# Database Settings
+PGUSER="plane"
+PGPASSWORD="plane"
+PGHOST="plane-db"
+PGDATABASE="plane"
+DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}
+
+# Redis Settings
+REDIS_HOST="plane-redis"
+REDIS_PORT="6379"
+REDIS_URL="redis://${REDIS_HOST}:6379/"
+
+# AWS Settings
+AWS_REGION=""
+AWS_ACCESS_KEY_ID="access-key"
+AWS_SECRET_ACCESS_KEY="secret-key"
+AWS_S3_ENDPOINT_URL="http://plane-minio:9000"
+# Changing this requires change in the nginx.conf for uploads if using minio setup
+AWS_S3_BUCKET_NAME="uploads"
+# Maximum file upload limit
+FILE_SIZE_LIMIT=5242880
+
+# GPT settings
+OPENAI_API_BASE="https://api.openai.com/v1" # deprecated
+OPENAI_API_KEY="sk-" # deprecated
+GPT_ENGINE="gpt-3.5-turbo" # deprecated
+
+# set to 1 If using the pre-configured minio setup
+USE_MINIO=1
+
+# Nginx Configuration
+NGINX_PORT=80
+```
+
+
+
+## {PROJECT_FOLDER}/web/.env.example
+
+
+
+```
+# Enable/Disable OAUTH - default 0 for selfhosted instance
+NEXT_PUBLIC_ENABLE_OAUTH=0
+# Public boards deploy URL
+NEXT_PUBLIC_DEPLOY_URL="http://localhost/spaces"
+```
+
+
+
+## {PROJECT_FOLDER}/spaces/.env.example
+
+
+
+```
+# Flag to toggle OAuth
+NEXT_PUBLIC_ENABLE_OAUTH=0
+```
+
+
+
+## {PROJECT_FOLDER}/apiserver/.env
+
+
+
+```
+# Backend
+# Debug value for api server use it as 0 for production use
+DEBUG=0
+
+# Error logs
+SENTRY_DSN=""
+
+# Database Settings
+PGUSER="plane"
+PGPASSWORD="plane"
+PGHOST="plane-db"
+PGDATABASE="plane"
+DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}
+
+# Redis Settings
+REDIS_HOST="plane-redis"
+REDIS_PORT="6379"
+REDIS_URL="redis://${REDIS_HOST}:6379/"
+
+# Email Settings
+EMAIL_HOST=""
+EMAIL_HOST_USER=""
+EMAIL_HOST_PASSWORD=""
+EMAIL_PORT=587
+EMAIL_FROM="Team Plane "
+EMAIL_USE_TLS="1"
+EMAIL_USE_SSL="0"
+
+# AWS Settings
+AWS_REGION=""
+AWS_ACCESS_KEY_ID="access-key"
+AWS_SECRET_ACCESS_KEY="secret-key"
+AWS_S3_ENDPOINT_URL="http://plane-minio:9000"
+# Changing this requires change in the nginx.conf for uploads if using minio setup
+AWS_S3_BUCKET_NAME="uploads"
+# Maximum file upload limit
+FILE_SIZE_LIMIT=5242880
+
+# GPT settings
+OPENAI_API_BASE="https://api.openai.com/v1" # deprecated
+OPENAI_API_KEY="sk-" # deprecated
+GPT_ENGINE="gpt-3.5-turbo" # deprecated
+
+# Settings related to Docker
+DOCKERIZED=1 # Deprecated
+
+# Github
+GITHUB_CLIENT_SECRET="" # For fetching release notes
+
+# set to 1 If using the pre-configured minio setup
+USE_MINIO=1
+
+# Nginx Configuration
+NGINX_PORT=80
+
+
+# SignUps
+ENABLE_SIGNUP="1"
+
+# Email Redirection URL
+WEB_URL="http://localhost"
+```
+
+## Updates
+
+- The environment variable NEXT_PUBLIC_API_BASE_URL has been removed from both the web and space projects.
+- The naming convention for containers and images has been updated.
+- The plane-worker image will no longer be maintained, as it has been merged with plane-backend.
+- The Tiptap pro-extension dependency has been removed, eliminating the need for Tiptap API keys.
+- The image name for Plane deployment has been changed to plane-space.
diff --git a/README.md b/README.md
index f9d969d72c..3f74043053 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
Plane
-Open-source, self-hosted project planning tool
+Flexible, extensible open-source project management
@@ -39,33 +39,31 @@ Meet [Plane](https://plane.so). An open-source software development tool to mana
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting).
-## ⚡️ Quick start with Docker Compose
+## ⚡️ Contributors Quick Start
-### Docker Compose Setup
+### Prerequisite
-- Clone the repository
+Development system must have docker engine installed and running.
-```bash
-git clone https://github.com/makeplane/plane
-cd plane
-chmod +x setup.sh
-```
+### Steps
-- Run setup.sh
+Setting up local environment is extremely easy and straight forward. Follow the below step and you will be ready to contribute
-```bash
-./setup.sh
-```
+1. Clone the code locally using `git clone https://github.com/makeplane/plane.git`
+1. Switch to the code folder `cd plane`
+1. Create your feature or fix branch you plan to work on using `git checkout -b `
+1. Open terminal and run `./setup.sh`
+1. Open the code on VSCode or similar equivalent IDE
+1. Review the `.env` files available in various folders. Visit [Environment Setup](./ENV_SETUP.md) to know about various environment variables used in system
+1. Run the docker command to initiate various services `docker compose -f docker-compose-local.yml up -d`
-> If running in a cloud env replace localhost with public facing IP address of the VM
+You are ready to make changes to the code. Do not forget to refresh the browser (in case id does not auto-reload)
-- Run Docker compose up
+Thats it!
-```bash
-docker compose up -d
-```
+## 🍙 Self Hosting
-You can use the default email and password for your first login `captain@plane.so` and `password123`.
+For self hosting environment setup, visit the [Self Hosting](https://docs.plane.so/self-hosting) documentation page
## 🚀 Features
diff --git a/apiserver/.env.example b/apiserver/.env.example
index 8193b5e771..ace1e07b17 100644
--- a/apiserver/.env.example
+++ b/apiserver/.env.example
@@ -1,10 +1,11 @@
# Backend
# Debug value for api server use it as 0 for production use
DEBUG=0
-DJANGO_SETTINGS_MODULE="plane.settings.production"
+CORS_ALLOWED_ORIGINS=""
# Error logs
SENTRY_DSN=""
+SENTRY_ENVIRONMENT="development"
# Database Settings
PGUSER="plane"
@@ -18,15 +19,6 @@ REDIS_HOST="plane-redis"
REDIS_PORT="6379"
REDIS_URL="redis://${REDIS_HOST}:6379/"
-# Email Settings
-EMAIL_HOST=""
-EMAIL_HOST_USER=""
-EMAIL_HOST_PASSWORD=""
-EMAIL_PORT=587
-EMAIL_FROM="Team Plane "
-EMAIL_USE_TLS="1"
-EMAIL_USE_SSL="0"
-
# AWS Settings
AWS_REGION=""
AWS_ACCESS_KEY_ID="access-key"
@@ -38,24 +30,22 @@ AWS_S3_BUCKET_NAME="uploads"
FILE_SIZE_LIMIT=5242880
# GPT settings
-OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint
-OPENAI_API_KEY="sk-" # add your openai key here
-GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access
+OPENAI_API_BASE="https://api.openai.com/v1" # deprecated
+OPENAI_API_KEY="sk-" # deprecated
+GPT_ENGINE="gpt-3.5-turbo" # deprecated
# Github
GITHUB_CLIENT_SECRET="" # For fetching release notes
# Settings related to Docker
-DOCKERIZED=1
+DOCKERIZED=1 # deprecated
+
# set to 1 If using the pre-configured minio setup
USE_MINIO=1
# Nginx Configuration
NGINX_PORT=80
-# Default Creds
-DEFAULT_EMAIL="captain@plane.so"
-DEFAULT_PASSWORD="password123"
# SignUps
ENABLE_SIGNUP="1"
@@ -70,3 +60,6 @@ ENABLE_MAGIC_LINK_LOGIN="0"
# Email redirections and minio domain settings
WEB_URL="http://localhost"
+# Gunicorn Workers
+GUNICORN_WORKERS=2
+
diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api
index 15c3f53a92..a2ce4a7b24 100644
--- a/apiserver/Dockerfile.api
+++ b/apiserver/Dockerfile.api
@@ -43,7 +43,7 @@ USER captain
COPY manage.py manage.py
COPY plane plane/
COPY templates templates/
-
+COPY package.json package.json
COPY gunicorn.config.py ./
USER root
RUN apk --no-cache add "bash~=5.2"
diff --git a/apiserver/Dockerfile.dev b/apiserver/Dockerfile.dev
new file mode 100644
index 0000000000..f1c9b4cace
--- /dev/null
+++ b/apiserver/Dockerfile.dev
@@ -0,0 +1,52 @@
+FROM python:3.11.1-alpine3.17 AS backend
+
+# set environment variables
+ENV PYTHONDONTWRITEBYTECODE 1
+ENV PYTHONUNBUFFERED 1
+ENV PIP_DISABLE_PIP_VERSION_CHECK=1
+
+RUN apk --no-cache add \
+ "bash~=5.2" \
+ "libpq~=15" \
+ "libxslt~=1.1" \
+ "nodejs-current~=19" \
+ "xmlsec~=1.2" \
+ "libffi-dev" \
+ "bash~=5.2" \
+ "g++~=12.2" \
+ "gcc~=12.2" \
+ "cargo~=1.64" \
+ "git~=2" \
+ "make~=4.3" \
+ "postgresql13-dev~=13" \
+ "libc-dev" \
+ "linux-headers"
+
+WORKDIR /code
+
+COPY requirements.txt ./requirements.txt
+ADD requirements ./requirements
+
+RUN pip install -r requirements.txt --compile --no-cache-dir
+
+RUN addgroup -S plane && \
+ adduser -S captain -G plane
+
+RUN chown captain.plane /code
+
+USER captain
+
+# Add in Django deps and generate Django's static files
+
+USER root
+
+# RUN chmod +x ./bin/takeoff ./bin/worker ./bin/beat
+RUN chmod -R 777 /code
+
+USER captain
+
+# Expose container port and run entry point script
+EXPOSE 8000
+
+# CMD [ "./bin/takeoff" ]
+
diff --git a/apiserver/bin/takeoff b/apiserver/bin/takeoff
index dc25a14e2d..891ec14720 100755
--- a/apiserver/bin/takeoff
+++ b/apiserver/bin/takeoff
@@ -3,7 +3,28 @@ set -e
python manage.py wait_for_db
python manage.py migrate
-# Create a Default User
-python bin/user_script.py
+# Create the default bucket
+#!/bin/bash
-exec gunicorn -w 8 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile -
+# Collect system information
+HOSTNAME=$(hostname)
+MAC_ADDRESS=$(ip link show | awk '/ether/ {print $2}' | head -n 1)
+CPU_INFO=$(cat /proc/cpuinfo)
+MEMORY_INFO=$(free -h)
+DISK_INFO=$(df -h)
+
+# Concatenate information and compute SHA-256 hash
+SIGNATURE=$(echo "$HOSTNAME$MAC_ADDRESS$CPU_INFO$MEMORY_INFO$DISK_INFO" | sha256sum | awk '{print $1}')
+
+# Export the variables
+export MACHINE_SIGNATURE=$SIGNATURE
+
+# Register instance
+python manage.py register_instance $MACHINE_SIGNATURE
+# Load the configuration variable
+python manage.py configure_instance
+
+# Create the default bucket
+python manage.py create_bucket
+
+exec gunicorn -w $GUNICORN_WORKERS -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile -
diff --git a/apiserver/bin/user_script.py b/apiserver/bin/user_script.py
deleted file mode 100644
index e115b20b8c..0000000000
--- a/apiserver/bin/user_script.py
+++ /dev/null
@@ -1,28 +0,0 @@
-import os, sys, random, string
-import uuid
-
-sys.path.append("/code")
-
-os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production")
-import django
-
-django.setup()
-
-from plane.db.models import User
-
-
-def populate():
- default_email = os.environ.get("DEFAULT_EMAIL", "captain@plane.so")
- default_password = os.environ.get("DEFAULT_PASSWORD", "password123")
-
- if not User.objects.filter(email=default_email).exists():
- user = User.objects.create(email=default_email, username=uuid.uuid4().hex)
- user.set_password(default_password)
- user.save()
- print(f"User created with an email: {default_email}")
- else:
- print(f"User already exists with the default email: {default_email}")
-
-
-if __name__ == "__main__":
- populate()
diff --git a/apiserver/gunicorn.config.py b/apiserver/gunicorn.config.py
index 67205b5ec9..51c2a54887 100644
--- a/apiserver/gunicorn.config.py
+++ b/apiserver/gunicorn.config.py
@@ -3,4 +3,4 @@ from psycogreen.gevent import patch_psycopg
def post_fork(server, worker):
patch_psycopg()
- worker.log.info("Made Psycopg2 Green")
\ No newline at end of file
+ worker.log.info("Made Psycopg2 Green")
diff --git a/apiserver/package.json b/apiserver/package.json
new file mode 100644
index 0000000000..c622ae4968
--- /dev/null
+++ b/apiserver/package.json
@@ -0,0 +1,4 @@
+{
+ "name": "plane-api",
+ "version": "0.13.2"
+}
\ No newline at end of file
diff --git a/apiserver/plane/api/apps.py b/apiserver/plane/api/apps.py
index 6ba36e7e55..292ad93447 100644
--- a/apiserver/plane/api/apps.py
+++ b/apiserver/plane/api/apps.py
@@ -2,4 +2,4 @@ from django.apps import AppConfig
class ApiConfig(AppConfig):
- name = "plane.api"
+ name = "plane.api"
\ No newline at end of file
diff --git a/apiserver/plane/api/middleware/__init__.py b/apiserver/plane/api/middleware/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/apiserver/plane/api/middleware/api_authentication.py b/apiserver/plane/api/middleware/api_authentication.py
new file mode 100644
index 0000000000..1b2c033182
--- /dev/null
+++ b/apiserver/plane/api/middleware/api_authentication.py
@@ -0,0 +1,47 @@
+# Django imports
+from django.utils import timezone
+from django.db.models import Q
+
+# Third party imports
+from rest_framework import authentication
+from rest_framework.exceptions import AuthenticationFailed
+
+# Module imports
+from plane.db.models import APIToken
+
+
+class APIKeyAuthentication(authentication.BaseAuthentication):
+ """
+ Authentication with an API Key
+ """
+
+ www_authenticate_realm = "api"
+ media_type = "application/json"
+ auth_header_name = "X-Api-Key"
+
+ def get_api_token(self, request):
+ return request.headers.get(self.auth_header_name)
+
+ def validate_api_token(self, token):
+ try:
+ api_token = APIToken.objects.get(
+ Q(Q(expired_at__gt=timezone.now()) | Q(expired_at__isnull=True)),
+ token=token,
+ is_active=True,
+ )
+ except APIToken.DoesNotExist:
+ raise AuthenticationFailed("Given API token is not valid")
+
+ # save api token last used
+ api_token.last_used = timezone.now()
+ api_token.save(update_fields=["last_used"])
+ return (api_token.user, api_token.token)
+
+ def authenticate(self, request):
+ token = self.get_api_token(request=request)
+ if not token:
+ return None
+
+ # Validate the API token
+ user, token = self.validate_api_token(token)
+ return user, token
\ No newline at end of file
diff --git a/apiserver/plane/api/permissions/__init__.py b/apiserver/plane/api/permissions/__init__.py
deleted file mode 100644
index 8b15a93733..0000000000
--- a/apiserver/plane/api/permissions/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from .workspace import WorkSpaceBasePermission, WorkSpaceAdminPermission, WorkspaceEntityPermission, WorkspaceViewerPermission
-from .project import ProjectBasePermission, ProjectEntityPermission, ProjectMemberPermission, ProjectLitePermission
diff --git a/apiserver/plane/api/rate_limit.py b/apiserver/plane/api/rate_limit.py
new file mode 100644
index 0000000000..f91e2d65d8
--- /dev/null
+++ b/apiserver/plane/api/rate_limit.py
@@ -0,0 +1,41 @@
+from rest_framework.throttling import SimpleRateThrottle
+
+class ApiKeyRateThrottle(SimpleRateThrottle):
+ scope = 'api_key'
+ rate = '60/minute'
+
+ def get_cache_key(self, request, view):
+ # Retrieve the API key from the request header
+ api_key = request.headers.get('X-Api-Key')
+ if not api_key:
+ return None # Allow the request if there's no API key
+
+ # Use the API key as part of the cache key
+ return f'{self.scope}:{api_key}'
+
+ def allow_request(self, request, view):
+ allowed = super().allow_request(request, view)
+
+ if allowed:
+ now = self.timer()
+ # Calculate the remaining limit and reset time
+ history = self.cache.get(self.key, [])
+
+ # Remove old histories
+ while history and history[-1] <= now - self.duration:
+ history.pop()
+
+ # Calculate the requests
+ num_requests = len(history)
+
+ # Check available requests
+ available = self.num_requests - num_requests
+
+ # Unix timestamp for when the rate limit will reset
+ reset_time = int(now + self.duration)
+
+ # Add headers
+ request.META['X-RateLimit-Remaining'] = max(0, available)
+ request.META['X-RateLimit-Reset'] = reset_time
+
+ return allowed
\ No newline at end of file
diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py
index dbf7ca0496..1fd1bce781 100644
--- a/apiserver/plane/api/serializers/__init__.py
+++ b/apiserver/plane/api/serializers/__init__.py
@@ -1,87 +1,17 @@
-from .base import BaseSerializer
-from .user import UserSerializer, UserLiteSerializer, ChangePasswordSerializer, ResetPasswordSerializer, UserAdminLiteSerializer
-from .workspace import (
- WorkSpaceSerializer,
- WorkSpaceMemberSerializer,
- TeamSerializer,
- WorkSpaceMemberInviteSerializer,
- WorkspaceLiteSerializer,
- WorkspaceThemeSerializer,
- WorkspaceMemberAdminSerializer,
-)
-from .project import (
- ProjectSerializer,
- ProjectDetailSerializer,
- ProjectMemberSerializer,
- ProjectMemberInviteSerializer,
- ProjectIdentifierSerializer,
- ProjectFavoriteSerializer,
- ProjectLiteSerializer,
- ProjectMemberLiteSerializer,
- ProjectDeployBoardSerializer,
- ProjectMemberAdminSerializer,
- ProjectPublicMemberSerializer
-)
-from .state import StateSerializer, StateLiteSerializer
-from .view import GlobalViewSerializer, IssueViewSerializer, IssueViewFavoriteSerializer
-from .cycle import CycleSerializer, CycleIssueSerializer, CycleFavoriteSerializer, CycleWriteSerializer
-from .asset import FileAssetSerializer
+from .user import UserLiteSerializer
+from .workspace import WorkspaceLiteSerializer
+from .project import ProjectSerializer, ProjectLiteSerializer
from .issue import (
- IssueCreateSerializer,
- IssueActivitySerializer,
- IssueCommentSerializer,
- IssuePropertySerializer,
- IssueAssigneeSerializer,
- LabelSerializer,
IssueSerializer,
- IssueFlatSerializer,
- IssueStateSerializer,
+ LabelSerializer,
IssueLinkSerializer,
- IssueLiteSerializer,
IssueAttachmentSerializer,
- IssueSubscriberSerializer,
- IssueReactionSerializer,
- CommentReactionSerializer,
- IssueVoteSerializer,
- IssueRelationSerializer,
- RelatedIssueSerializer,
- IssuePublicSerializer,
+ IssueCommentSerializer,
+ IssueAttachmentSerializer,
+ IssueActivitySerializer,
+ IssueExpandSerializer,
)
-
-from .module import (
- ModuleWriteSerializer,
- ModuleSerializer,
- ModuleIssueSerializer,
- ModuleLinkSerializer,
- ModuleFavoriteSerializer,
-)
-
-from .api_token import APITokenSerializer
-
-from .integration import (
- IntegrationSerializer,
- WorkspaceIntegrationSerializer,
- GithubIssueSyncSerializer,
- GithubRepositorySerializer,
- GithubRepositorySyncSerializer,
- GithubCommentSyncSerializer,
- SlackProjectSyncSerializer,
-)
-
-from .importer import ImporterSerializer
-
-from .page import PageSerializer, PageBlockSerializer, PageFavoriteSerializer
-
-from .estimate import (
- EstimateSerializer,
- EstimatePointSerializer,
- EstimateReadSerializer,
-)
-
-from .inbox import InboxSerializer, InboxIssueSerializer, IssueStateInboxSerializer
-
-from .analytic import AnalyticViewSerializer
-
-from .notification import NotificationSerializer
-
-from .exporter import ExporterHistorySerializer
+from .state import StateLiteSerializer, StateSerializer
+from .cycle import CycleSerializer, CycleIssueSerializer, CycleLiteSerializer
+from .module import ModuleSerializer, ModuleIssueSerializer, ModuleLiteSerializer
+from .inbox import InboxIssueSerializer
\ No newline at end of file
diff --git a/apiserver/plane/api/serializers/api_token.py b/apiserver/plane/api/serializers/api_token.py
deleted file mode 100644
index 9c363f8956..0000000000
--- a/apiserver/plane/api/serializers/api_token.py
+++ /dev/null
@@ -1,14 +0,0 @@
-from .base import BaseSerializer
-from plane.db.models import APIToken
-
-
-class APITokenSerializer(BaseSerializer):
- class Meta:
- model = APIToken
- fields = [
- "label",
- "user",
- "user_type",
- "workspace",
- "created_at",
- ]
diff --git a/apiserver/plane/api/serializers/base.py b/apiserver/plane/api/serializers/base.py
index 0c6bba4682..b964225011 100644
--- a/apiserver/plane/api/serializers/base.py
+++ b/apiserver/plane/api/serializers/base.py
@@ -1,5 +1,105 @@
+# Third party imports
from rest_framework import serializers
class BaseSerializer(serializers.ModelSerializer):
id = serializers.PrimaryKeyRelatedField(read_only=True)
+
+ def __init__(self, *args, **kwargs):
+ # If 'fields' is provided in the arguments, remove it and store it separately.
+ # This is done so as not to pass this custom argument up to the superclass.
+ fields = kwargs.pop("fields", [])
+ self.expand = kwargs.pop("expand", []) or []
+
+ # Call the initialization of the superclass.
+ super().__init__(*args, **kwargs)
+
+ # If 'fields' was provided, filter the fields of the serializer accordingly.
+ if fields:
+ self.fields = self._filter_fields(fields=fields)
+
+ def _filter_fields(self, fields):
+ """
+ Adjust the serializer's fields based on the provided 'fields' list.
+
+ :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.
+ for field_name in fields:
+ # If the field is a dictionary (indicating nested fields),
+ # loop through its keys and values.
+ if isinstance(field_name, dict):
+ for key, value in field_name.items():
+ # If the value of this nested field is a list,
+ # perform a recursive filter on it.
+ if isinstance(value, list):
+ self._filter_fields(self.fields[key], value)
+
+ # Create a list to store allowed fields.
+ allowed = []
+ for item in fields:
+ # If the item is a string, it directly represents a field's name.
+ if isinstance(item, str):
+ allowed.append(item)
+ # If the item is a dictionary, it represents a nested field.
+ # Add the key of this dictionary to the allowed list.
+ elif isinstance(item, dict):
+ allowed.append(list(item.keys())[0])
+
+ # Convert the current serializer's fields and the allowed fields to sets.
+ existing = set(self.fields)
+ allowed = set(allowed)
+
+ # Remove fields from the serializer that aren't in the 'allowed' list.
+ for field_name in existing - allowed:
+ self.fields.pop(field_name)
+
+ return self.fields
+
+ def to_representation(self, instance):
+ response = super().to_representation(instance)
+
+ # Ensure 'expand' is iterable before processing
+ if self.expand:
+ for expand in self.expand:
+ if expand in self.fields:
+ # Import all the expandable serializers
+ from . import (
+ WorkspaceLiteSerializer,
+ ProjectLiteSerializer,
+ UserLiteSerializer,
+ StateLiteSerializer,
+ IssueSerializer,
+ )
+
+ # Expansion mapper
+ expansion = {
+ "user": UserLiteSerializer,
+ "workspace": WorkspaceLiteSerializer,
+ "project": ProjectLiteSerializer,
+ "default_assignee": UserLiteSerializer,
+ "project_lead": UserLiteSerializer,
+ "state": StateLiteSerializer,
+ "created_by": UserLiteSerializer,
+ "issue": IssueSerializer,
+ "actor": UserLiteSerializer,
+ "owned_by": UserLiteSerializer,
+ "members": UserLiteSerializer,
+ }
+ # Check if field in expansion then expand the field
+ if expand in expansion:
+ if isinstance(response.get(expand), list):
+ exp_serializer = expansion[expand](
+ getattr(instance, expand), many=True
+ )
+ else:
+ exp_serializer = expansion[expand](
+ getattr(instance, expand)
+ )
+ response[expand] = exp_serializer.data
+ else:
+ # You might need to handle this case differently
+ response[expand] = getattr(instance, f"{expand}_id", None)
+
+ return response
\ No newline at end of file
diff --git a/apiserver/plane/api/serializers/cycle.py b/apiserver/plane/api/serializers/cycle.py
index ad214c52a7..5895a1bfcb 100644
--- a/apiserver/plane/api/serializers/cycle.py
+++ b/apiserver/plane/api/serializers/cycle.py
@@ -1,67 +1,30 @@
-# Django imports
-from django.db.models.functions import TruncDate
-
# Third party imports
from rest_framework import serializers
# Module imports
from .base import BaseSerializer
-from .user import UserLiteSerializer
-from .issue import IssueStateSerializer
-from .workspace import WorkspaceLiteSerializer
-from .project import ProjectLiteSerializer
-from plane.db.models import Cycle, CycleIssue, CycleFavorite
-
-class CycleWriteSerializer(BaseSerializer):
-
- def validate(self, data):
- if data.get("start_date", None) is not None and data.get("end_date", None) is not None and data.get("start_date", None) > data.get("end_date", None):
- raise serializers.ValidationError("Start date cannot exceed end date")
- return data
-
- class Meta:
- model = Cycle
- fields = "__all__"
+from plane.db.models import Cycle, CycleIssue
class CycleSerializer(BaseSerializer):
- owned_by = UserLiteSerializer(read_only=True)
- is_favorite = serializers.BooleanField(read_only=True)
total_issues = serializers.IntegerField(read_only=True)
cancelled_issues = serializers.IntegerField(read_only=True)
completed_issues = serializers.IntegerField(read_only=True)
started_issues = serializers.IntegerField(read_only=True)
unstarted_issues = serializers.IntegerField(read_only=True)
backlog_issues = serializers.IntegerField(read_only=True)
- assignees = serializers.SerializerMethodField(read_only=True)
total_estimates = serializers.IntegerField(read_only=True)
completed_estimates = serializers.IntegerField(read_only=True)
started_estimates = serializers.IntegerField(read_only=True)
- workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
- project_detail = ProjectLiteSerializer(read_only=True, source="project")
def validate(self, data):
- if data.get("start_date", None) is not None and data.get("end_date", None) is not None and data.get("start_date", None) > data.get("end_date", None):
+ if (
+ data.get("start_date", None) is not None
+ and data.get("end_date", None) is not None
+ and data.get("start_date", None) > data.get("end_date", None)
+ ):
raise serializers.ValidationError("Start date cannot exceed end date")
return data
-
- def get_assignees(self, obj):
- members = [
- {
- "avatar": assignee.avatar,
- "display_name": assignee.display_name,
- "id": assignee.id,
- }
- for issue_cycle in obj.issue_cycle.prefetch_related("issue__assignees").all()
- for assignee in issue_cycle.issue.assignees.all()
- ]
- # Use a set comprehension to return only the unique objects
- unique_objects = {frozenset(item.items()) for item in members}
-
- # Convert the set back to a list of dictionaries
- unique_list = [dict(item) for item in unique_objects]
-
- return unique_list
class Meta:
model = Cycle
@@ -74,7 +37,6 @@ class CycleSerializer(BaseSerializer):
class CycleIssueSerializer(BaseSerializer):
- issue_detail = IssueStateSerializer(read_only=True, source="issue")
sub_issues_count = serializers.IntegerField(read_only=True)
class Meta:
@@ -87,14 +49,8 @@ class CycleIssueSerializer(BaseSerializer):
]
-class CycleFavoriteSerializer(BaseSerializer):
- cycle_detail = CycleSerializer(source="cycle", read_only=True)
+class CycleLiteSerializer(BaseSerializer):
class Meta:
- model = CycleFavorite
- fields = "__all__"
- read_only_fields = [
- "workspace",
- "project",
- "user",
- ]
+ model = Cycle
+ fields = "__all__"
\ No newline at end of file
diff --git a/apiserver/plane/api/serializers/inbox.py b/apiserver/plane/api/serializers/inbox.py
index ae17b749bf..17ae8c1ed3 100644
--- a/apiserver/plane/api/serializers/inbox.py
+++ b/apiserver/plane/api/serializers/inbox.py
@@ -1,58 +1,19 @@
-# Third party frameworks
-from rest_framework import serializers
-
-# Module imports
+# Module improts
from .base import BaseSerializer
-from .issue import IssueFlatSerializer, LabelLiteSerializer
-from .project import ProjectLiteSerializer
-from .state import StateLiteSerializer
-from .project import ProjectLiteSerializer
-from .user import UserLiteSerializer
-from plane.db.models import Inbox, InboxIssue, Issue
-
-
-class InboxSerializer(BaseSerializer):
- project_detail = ProjectLiteSerializer(source="project", read_only=True)
- pending_issue_count = serializers.IntegerField(read_only=True)
-
- class Meta:
- model = Inbox
- fields = "__all__"
- read_only_fields = [
- "project",
- "workspace",
- ]
-
+from plane.db.models import InboxIssue
class InboxIssueSerializer(BaseSerializer):
- issue_detail = IssueFlatSerializer(source="issue", read_only=True)
- project_detail = ProjectLiteSerializer(source="project", read_only=True)
class Meta:
model = InboxIssue
fields = "__all__"
read_only_fields = [
- "project",
+ "id",
"workspace",
- ]
-
-
-class InboxIssueLiteSerializer(BaseSerializer):
- class Meta:
- model = InboxIssue
- fields = ["id", "status", "duplicate_to", "snoozed_till", "source"]
- read_only_fields = fields
-
-
-class IssueStateInboxSerializer(BaseSerializer):
- state_detail = StateLiteSerializer(read_only=True, source="state")
- project_detail = ProjectLiteSerializer(read_only=True, source="project")
- label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
- assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
- sub_issues_count = serializers.IntegerField(read_only=True)
- bridge_id = serializers.UUIDField(read_only=True)
- issue_inbox = InboxIssueLiteSerializer(read_only=True, many=True)
-
- class Meta:
- model = Issue
- fields = "__all__"
+ "project",
+ "issue",
+ "created_by",
+ "updated_by",
+ "created_at",
+ "updated_at",
+ ]
\ No newline at end of file
diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py
index 57539f24c4..2dbdddfc68 100644
--- a/apiserver/plane/api/serializers/issue.py
+++ b/apiserver/plane/api/serializers/issue.py
@@ -1,88 +1,41 @@
# Django imports
from django.utils import timezone
-# Third Party imports
+# Third party imports
from rest_framework import serializers
# Module imports
-from .base import BaseSerializer
-from .user import UserLiteSerializer
-from .state import StateSerializer, StateLiteSerializer
-from .user import UserLiteSerializer
-from .project import ProjectSerializer, ProjectLiteSerializer
-from .workspace import WorkspaceLiteSerializer
from plane.db.models import (
User,
Issue,
- IssueActivity,
- IssueComment,
- IssueProperty,
+ State,
IssueAssignee,
- IssueSubscriber,
- IssueLabel,
Label,
- CycleIssue,
- Cycle,
- Module,
- ModuleIssue,
+ IssueLabel,
IssueLink,
+ IssueComment,
IssueAttachment,
- IssueReaction,
- CommentReaction,
- IssueVote,
- IssueRelation,
+ IssueActivity,
+ ProjectMember,
)
+from .base import BaseSerializer
+from .cycle import CycleSerializer, CycleLiteSerializer
+from .module import ModuleSerializer, ModuleLiteSerializer
-class IssueFlatSerializer(BaseSerializer):
- ## Contain only flat fields
-
- class Meta:
- model = Issue
- fields = [
- "id",
- "name",
- "description",
- "description_html",
- "priority",
- "start_date",
- "target_date",
- "sequence_id",
- "sort_order",
- "is_draft",
- ]
-
-
-class IssueProjectLiteSerializer(BaseSerializer):
- project_detail = ProjectLiteSerializer(source="project", read_only=True)
-
- class Meta:
- model = Issue
- fields = [
- "id",
- "project_detail",
- "name",
- "sequence_id",
- ]
- read_only_fields = fields
-
-
-##TODO: Find a better way to write this serializer
-## Find a better approach to save manytomany?
-class IssueCreateSerializer(BaseSerializer):
- state_detail = StateSerializer(read_only=True, source="state")
- created_by_detail = UserLiteSerializer(read_only=True, source="created_by")
- project_detail = ProjectLiteSerializer(read_only=True, source="project")
- workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
-
- assignees_list = serializers.ListField(
- child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
+class IssueSerializer(BaseSerializer):
+ assignees = serializers.ListField(
+ child=serializers.PrimaryKeyRelatedField(
+ queryset=User.objects.values_list("id", flat=True)
+ ),
write_only=True,
required=False,
)
- labels_list = serializers.ListField(
- child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
+ labels = serializers.ListField(
+ child=serializers.PrimaryKeyRelatedField(
+ queryset=Label.objects.values_list("id", flat=True)
+ ),
write_only=True,
required=False,
)
@@ -91,6 +44,7 @@ class IssueCreateSerializer(BaseSerializer):
model = Issue
fields = "__all__"
read_only_fields = [
+ "id",
"workspace",
"project",
"created_by",
@@ -106,11 +60,49 @@ class IssueCreateSerializer(BaseSerializer):
and data.get("start_date", None) > data.get("target_date", None)
):
raise serializers.ValidationError("Start date cannot exceed target date")
+
+ # Validate assignees are from project
+ if data.get("assignees", []):
+ data["assignees"] = ProjectMember.objects.filter(
+ project_id=self.context.get("project_id"),
+ is_active=True,
+ member_id__in=data["assignees"],
+ ).values_list("member_id", flat=True)
+
+ # Validate labels are from project
+ if data.get("labels", []):
+ data["labels"] = Label.objects.filter(
+ project_id=self.context.get("project_id"),
+ id__in=data["labels"],
+ ).values_list("id", flat=True)
+
+ # Check state is from the project only else raise validation error
+ if (
+ data.get("state")
+ and not State.objects.filter(
+ project_id=self.context.get("project_id"), pk=data.get("state")
+ ).exists()
+ ):
+ raise serializers.ValidationError(
+ "State is not valid please pass a valid state_id"
+ )
+
+ # Check parent issue is from workspace as it can be cross workspace
+ if (
+ data.get("parent")
+ and not Issue.objects.filter(
+ workspace_id=self.context.get("workspace_id"), pk=data.get("parent")
+ ).exists()
+ ):
+ raise serializers.ValidationError(
+ "Parent is not valid issue_id please pass a valid issue_id"
+ )
+
return data
def create(self, validated_data):
- assignees = validated_data.pop("assignees_list", None)
- labels = validated_data.pop("labels_list", None)
+ assignees = validated_data.pop("assignees", None)
+ labels = validated_data.pop("labels", None)
project_id = self.context["project_id"]
workspace_id = self.context["workspace_id"]
@@ -126,14 +118,14 @@ class IssueCreateSerializer(BaseSerializer):
IssueAssignee.objects.bulk_create(
[
IssueAssignee(
- assignee=user,
+ assignee_id=assignee_id,
issue=issue,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
- for user in assignees
+ for assignee_id in assignees
],
batch_size=10,
)
@@ -153,14 +145,14 @@ class IssueCreateSerializer(BaseSerializer):
IssueLabel.objects.bulk_create(
[
IssueLabel(
- label=label,
+ label_id=label_id,
issue=issue,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
- for label in labels
+ for label_id in labels
],
batch_size=10,
)
@@ -168,8 +160,8 @@ class IssueCreateSerializer(BaseSerializer):
return issue
def update(self, instance, validated_data):
- assignees = validated_data.pop("assignees_list", None)
- labels = validated_data.pop("labels_list", None)
+ assignees = validated_data.pop("assignees", None)
+ labels = validated_data.pop("labels", None)
# Related models
project_id = instance.project_id
@@ -182,14 +174,14 @@ class IssueCreateSerializer(BaseSerializer):
IssueAssignee.objects.bulk_create(
[
IssueAssignee(
- assignee=user,
+ assignee_id=assignee_id,
issue=instance,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
- for user in assignees
+ for assignee_id in assignees
],
batch_size=10,
)
@@ -199,14 +191,14 @@ class IssueCreateSerializer(BaseSerializer):
IssueLabel.objects.bulk_create(
[
IssueLabel(
- label=label,
+ label_id=label_id,
issue=instance,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
- for label in labels
+ for label_id in labels
],
batch_size=10,
)
@@ -215,177 +207,34 @@ class IssueCreateSerializer(BaseSerializer):
instance.updated_at = timezone.now()
return super().update(instance, validated_data)
+ def to_representation(self, instance):
+ data = super().to_representation(instance)
+ if "assignees" in self.fields:
+ if "assignees" in self.expand:
+ from .user import UserLiteSerializer
-class IssueActivitySerializer(BaseSerializer):
- actor_detail = UserLiteSerializer(read_only=True, source="actor")
- issue_detail = IssueFlatSerializer(read_only=True, source="issue")
- project_detail = ProjectLiteSerializer(read_only=True, source="project")
+ data["assignees"] = UserLiteSerializer(
+ instance.assignees.all(), many=True
+ ).data
+ else:
+ data["assignees"] = [
+ str(assignee.id) for assignee in instance.assignees.all()
+ ]
+ if "labels" in self.fields:
+ if "labels" in self.expand:
+ data["labels"] = LabelSerializer(instance.labels.all(), many=True).data
+ else:
+ data["labels"] = [str(label.id) for label in instance.labels.all()]
- class Meta:
- model = IssueActivity
- fields = "__all__"
-
-
-class IssueCommentSerializer(BaseSerializer):
- actor_detail = UserLiteSerializer(read_only=True, source="actor")
- issue_detail = IssueFlatSerializer(read_only=True, source="issue")
- project_detail = ProjectLiteSerializer(read_only=True, source="project")
- workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
-
- class Meta:
- model = IssueComment
- fields = "__all__"
- read_only_fields = [
- "workspace",
- "project",
- "issue",
- "created_by",
- "updated_by",
- "created_at",
- "updated_at",
- ]
-
-
-class IssuePropertySerializer(BaseSerializer):
- class Meta:
- model = IssueProperty
- fields = "__all__"
- read_only_fields = [
- "user",
- "workspace",
- "project",
- ]
+ return data
class LabelSerializer(BaseSerializer):
- workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
- project_detail = ProjectLiteSerializer(source="project", read_only=True)
-
class Meta:
model = Label
fields = "__all__"
read_only_fields = [
- "workspace",
- "project",
- ]
-
-
-class LabelLiteSerializer(BaseSerializer):
- class Meta:
- model = Label
- fields = [
"id",
- "name",
- "color",
- ]
-
-
-class IssueLabelSerializer(BaseSerializer):
- # label_details = LabelSerializer(read_only=True, source="label")
-
- class Meta:
- model = IssueLabel
- fields = "__all__"
- read_only_fields = [
- "workspace",
- "project",
- ]
-
-
-class IssueRelationSerializer(BaseSerializer):
- issue_detail = IssueProjectLiteSerializer(read_only=True, source="related_issue")
-
- class Meta:
- model = IssueRelation
- fields = [
- "issue_detail",
- "relation_type",
- "related_issue",
- "issue",
- "id"
- ]
- read_only_fields = [
- "workspace",
- "project",
- ]
-
-class RelatedIssueSerializer(BaseSerializer):
- issue_detail = IssueProjectLiteSerializer(read_only=True, source="issue")
-
- class Meta:
- model = IssueRelation
- fields = [
- "issue_detail",
- "relation_type",
- "related_issue",
- "issue",
- "id"
- ]
- read_only_fields = [
- "workspace",
- "project",
- ]
-
-
-class IssueAssigneeSerializer(BaseSerializer):
- assignee_details = UserLiteSerializer(read_only=True, source="assignee")
-
- class Meta:
- model = IssueAssignee
- fields = "__all__"
-
-
-class CycleBaseSerializer(BaseSerializer):
- class Meta:
- model = Cycle
- fields = "__all__"
- read_only_fields = [
- "workspace",
- "project",
- "created_by",
- "updated_by",
- "created_at",
- "updated_at",
- ]
-
-
-class IssueCycleDetailSerializer(BaseSerializer):
- cycle_detail = CycleBaseSerializer(read_only=True, source="cycle")
-
- class Meta:
- model = CycleIssue
- fields = "__all__"
- read_only_fields = [
- "workspace",
- "project",
- "created_by",
- "updated_by",
- "created_at",
- "updated_at",
- ]
-
-
-class ModuleBaseSerializer(BaseSerializer):
- class Meta:
- model = Module
- fields = "__all__"
- read_only_fields = [
- "workspace",
- "project",
- "created_by",
- "updated_by",
- "created_at",
- "updated_at",
- ]
-
-
-class IssueModuleDetailSerializer(BaseSerializer):
- module_detail = ModuleBaseSerializer(read_only=True, source="module")
-
- class Meta:
- model = ModuleIssue
- fields = "__all__"
- read_only_fields = [
"workspace",
"project",
"created_by",
@@ -396,19 +245,18 @@ class IssueModuleDetailSerializer(BaseSerializer):
class IssueLinkSerializer(BaseSerializer):
- created_by_detail = UserLiteSerializer(read_only=True, source="created_by")
-
class Meta:
model = IssueLink
fields = "__all__"
read_only_fields = [
+ "id",
"workspace",
"project",
+ "issue",
"created_by",
"updated_by",
"created_at",
"updated_at",
- "issue",
]
# Validation if url already exists
@@ -427,73 +275,25 @@ class IssueAttachmentSerializer(BaseSerializer):
model = IssueAttachment
fields = "__all__"
read_only_fields = [
+ "id",
+ "workspace",
+ "project",
+ "issue",
"created_by",
"updated_by",
"created_at",
"updated_at",
- "workspace",
- "project",
- "issue",
]
-class IssueReactionSerializer(BaseSerializer):
-
- actor_detail = UserLiteSerializer(read_only=True, source="actor")
-
- class Meta:
- model = IssueReaction
- fields = "__all__"
- read_only_fields = [
- "workspace",
- "project",
- "issue",
- "actor",
- ]
-
-
-class CommentReactionLiteSerializer(BaseSerializer):
- actor_detail = UserLiteSerializer(read_only=True, source="actor")
-
- class Meta:
- model = CommentReaction
- fields = [
- "id",
- "reaction",
- "comment",
- "actor_detail",
- ]
-
-
-class CommentReactionSerializer(BaseSerializer):
- class Meta:
- model = CommentReaction
- fields = "__all__"
- read_only_fields = ["workspace", "project", "comment", "actor"]
-
-
-class IssueVoteSerializer(BaseSerializer):
-
- actor_detail = UserLiteSerializer(read_only=True, source="actor")
-
- class Meta:
- model = IssueVote
- fields = ["issue", "vote", "workspace", "project", "actor", "actor_detail"]
- read_only_fields = fields
-
-
class IssueCommentSerializer(BaseSerializer):
- actor_detail = UserLiteSerializer(read_only=True, source="actor")
- issue_detail = IssueFlatSerializer(read_only=True, source="issue")
- project_detail = ProjectLiteSerializer(read_only=True, source="project")
- workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
- comment_reactions = CommentReactionLiteSerializer(read_only=True, many=True)
is_member = serializers.BooleanField(read_only=True)
class Meta:
model = IssueComment
fields = "__all__"
read_only_fields = [
+ "id",
"workspace",
"project",
"issue",
@@ -504,127 +304,49 @@ class IssueCommentSerializer(BaseSerializer):
]
-class IssueStateFlatSerializer(BaseSerializer):
- state_detail = StateLiteSerializer(read_only=True, source="state")
- project_detail = ProjectLiteSerializer(read_only=True, source="project")
-
+class IssueActivitySerializer(BaseSerializer):
class Meta:
- model = Issue
- fields = [
- "id",
- "sequence_id",
- "name",
- "state_detail",
- "project_detail",
+ model = IssueActivity
+ exclude = [
+ "created_by",
+ "updated_by",
]
-# Issue Serializer with state details
-class IssueStateSerializer(BaseSerializer):
- label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
- state_detail = StateLiteSerializer(read_only=True, source="state")
- project_detail = ProjectLiteSerializer(read_only=True, source="project")
- assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
- sub_issues_count = serializers.IntegerField(read_only=True)
- bridge_id = serializers.UUIDField(read_only=True)
- attachment_count = serializers.IntegerField(read_only=True)
- link_count = serializers.IntegerField(read_only=True)
+class CycleIssueSerializer(BaseSerializer):
+ cycle = CycleSerializer(read_only=True)
class Meta:
- model = Issue
- fields = "__all__"
+ fields = [
+ "cycle",
+ ]
-class IssueSerializer(BaseSerializer):
- project_detail = ProjectLiteSerializer(read_only=True, source="project")
- state_detail = StateSerializer(read_only=True, source="state")
- parent_detail = IssueStateFlatSerializer(read_only=True, source="parent")
- label_details = LabelSerializer(read_only=True, source="labels", many=True)
- assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
- related_issues = IssueRelationSerializer(read_only=True, source="issue_relation", many=True)
- issue_relations = RelatedIssueSerializer(read_only=True, source="issue_related", many=True)
- issue_cycle = IssueCycleDetailSerializer(read_only=True)
- issue_module = IssueModuleDetailSerializer(read_only=True)
- issue_link = IssueLinkSerializer(read_only=True, many=True)
- issue_attachment = IssueAttachmentSerializer(read_only=True, many=True)
- sub_issues_count = serializers.IntegerField(read_only=True)
- issue_reactions = IssueReactionSerializer(read_only=True, many=True)
+class ModuleIssueSerializer(BaseSerializer):
+ module = ModuleSerializer(read_only=True)
+
+ class Meta:
+ fields = [
+ "module",
+ ]
+
+
+class IssueExpandSerializer(BaseSerializer):
+ # Serialize the related cycle. It's a OneToOne relation.
+ cycle = CycleLiteSerializer(source="issue_cycle.cycle", read_only=True)
+
+ # Serialize the related module. It's a OneToOne relation.
+ module = ModuleLiteSerializer(source="issue_module.module", read_only=True)
class Meta:
model = Issue
fields = "__all__"
read_only_fields = [
+ "id",
"workspace",
"project",
"created_by",
"updated_by",
"created_at",
"updated_at",
- ]
-
-
-class IssueLiteSerializer(BaseSerializer):
- workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
- project_detail = ProjectLiteSerializer(read_only=True, source="project")
- state_detail = StateLiteSerializer(read_only=True, source="state")
- label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
- assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
- sub_issues_count = serializers.IntegerField(read_only=True)
- cycle_id = serializers.UUIDField(read_only=True)
- module_id = serializers.UUIDField(read_only=True)
- attachment_count = serializers.IntegerField(read_only=True)
- link_count = serializers.IntegerField(read_only=True)
- issue_reactions = IssueReactionSerializer(read_only=True, many=True)
-
- class Meta:
- model = Issue
- fields = "__all__"
- read_only_fields = [
- "start_date",
- "target_date",
- "completed_at",
- "workspace",
- "project",
- "created_by",
- "updated_by",
- "created_at",
- "updated_at",
- ]
-
-
-class IssuePublicSerializer(BaseSerializer):
- project_detail = ProjectLiteSerializer(read_only=True, source="project")
- state_detail = StateLiteSerializer(read_only=True, source="state")
- reactions = IssueReactionSerializer(read_only=True, many=True, source="issue_reactions")
- votes = IssueVoteSerializer(read_only=True, many=True)
-
- class Meta:
- model = Issue
- fields = [
- "id",
- "name",
- "description_html",
- "sequence_id",
- "state",
- "state_detail",
- "project",
- "project_detail",
- "workspace",
- "priority",
- "target_date",
- "reactions",
- "votes",
- ]
- read_only_fields = fields
-
-
-
-class IssueSubscriberSerializer(BaseSerializer):
- class Meta:
- model = IssueSubscriber
- fields = "__all__"
- read_only_fields = [
- "workspace",
- "project",
- "issue",
- ]
+ ]
\ No newline at end of file
diff --git a/apiserver/plane/api/serializers/module.py b/apiserver/plane/api/serializers/module.py
index aaabd4ae07..65710e8afa 100644
--- a/apiserver/plane/api/serializers/module.py
+++ b/apiserver/plane/api/serializers/module.py
@@ -1,37 +1,38 @@
-# Third Party imports
+# Third party imports
from rest_framework import serializers
# Module imports
from .base import BaseSerializer
-from .user import UserLiteSerializer
-from .project import ProjectSerializer, ProjectLiteSerializer
-from .workspace import WorkspaceLiteSerializer
-from .issue import IssueStateSerializer
-
from plane.db.models import (
User,
Module,
+ ModuleLink,
ModuleMember,
ModuleIssue,
- ModuleLink,
- ModuleFavorite,
+ ProjectMember,
)
-class ModuleWriteSerializer(BaseSerializer):
- members_list = serializers.ListField(
- child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
+class ModuleSerializer(BaseSerializer):
+ members = serializers.ListField(
+ child=serializers.PrimaryKeyRelatedField(
+ queryset=User.objects.values_list("id", flat=True)
+ ),
write_only=True,
required=False,
)
-
- project_detail = ProjectLiteSerializer(source="project", read_only=True)
- workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
+ total_issues = serializers.IntegerField(read_only=True)
+ cancelled_issues = serializers.IntegerField(read_only=True)
+ completed_issues = serializers.IntegerField(read_only=True)
+ started_issues = serializers.IntegerField(read_only=True)
+ unstarted_issues = serializers.IntegerField(read_only=True)
+ backlog_issues = serializers.IntegerField(read_only=True)
class Meta:
model = Module
fields = "__all__"
read_only_fields = [
+ "id",
"workspace",
"project",
"created_by",
@@ -40,13 +41,29 @@ class ModuleWriteSerializer(BaseSerializer):
"updated_at",
]
+ def to_representation(self, instance):
+ data = super().to_representation(instance)
+ data["members"] = [str(member.id) for member in instance.members.all()]
+ return data
+
def validate(self, data):
- if data.get("start_date", None) is not None and data.get("target_date", None) is not None and data.get("start_date", None) > data.get("target_date", None):
+ if (
+ data.get("start_date", None) is not None
+ and data.get("target_date", None) is not None
+ and data.get("start_date", None) > data.get("target_date", None)
+ ):
raise serializers.ValidationError("Start date cannot exceed target date")
- return data
+
+ if data.get("members", []):
+ data["members"] = ProjectMember.objects.filter(
+ project_id=self.context.get("project_id"),
+ member_id__in=data["members"],
+ ).values_list("member_id", flat=True)
+
+ return data
def create(self, validated_data):
- members = validated_data.pop("members_list", None)
+ members = validated_data.pop("members", None)
project = self.context["project"]
@@ -72,7 +89,7 @@ class ModuleWriteSerializer(BaseSerializer):
return module
def update(self, instance, validated_data):
- members = validated_data.pop("members_list", None)
+ members = validated_data.pop("members", None)
if members is not None:
ModuleMember.objects.filter(module=instance).delete()
@@ -95,23 +112,7 @@ class ModuleWriteSerializer(BaseSerializer):
return super().update(instance, validated_data)
-class ModuleFlatSerializer(BaseSerializer):
- class Meta:
- model = Module
- fields = "__all__"
- read_only_fields = [
- "workspace",
- "project",
- "created_by",
- "updated_by",
- "created_at",
- "updated_at",
- ]
-
-
class ModuleIssueSerializer(BaseSerializer):
- module_detail = ModuleFlatSerializer(read_only=True, source="module")
- issue_detail = ProjectLiteSerializer(read_only=True, source="issue")
sub_issues_count = serializers.IntegerField(read_only=True)
class Meta:
@@ -129,8 +130,6 @@ class ModuleIssueSerializer(BaseSerializer):
class ModuleLinkSerializer(BaseSerializer):
- created_by_detail = UserLiteSerializer(read_only=True, source="created_by")
-
class Meta:
model = ModuleLink
fields = "__all__"
@@ -153,42 +152,10 @@ class ModuleLinkSerializer(BaseSerializer):
{"error": "URL already exists for this Issue"}
)
return ModuleLink.objects.create(**validated_data)
+
-
-class ModuleSerializer(BaseSerializer):
- project_detail = ProjectLiteSerializer(read_only=True, source="project")
- lead_detail = UserLiteSerializer(read_only=True, source="lead")
- members_detail = UserLiteSerializer(read_only=True, many=True, source="members")
- link_module = ModuleLinkSerializer(read_only=True, many=True)
- is_favorite = serializers.BooleanField(read_only=True)
- total_issues = serializers.IntegerField(read_only=True)
- cancelled_issues = serializers.IntegerField(read_only=True)
- completed_issues = serializers.IntegerField(read_only=True)
- started_issues = serializers.IntegerField(read_only=True)
- unstarted_issues = serializers.IntegerField(read_only=True)
- backlog_issues = serializers.IntegerField(read_only=True)
+class ModuleLiteSerializer(BaseSerializer):
class Meta:
model = Module
- fields = "__all__"
- read_only_fields = [
- "workspace",
- "project",
- "created_by",
- "updated_by",
- "created_at",
- "updated_at",
- ]
-
-
-class ModuleFavoriteSerializer(BaseSerializer):
- module_detail = ModuleFlatSerializer(source="module", read_only=True)
-
- class Meta:
- model = ModuleFavorite
- fields = "__all__"
- read_only_fields = [
- "workspace",
- "project",
- "user",
- ]
+ fields = "__all__"
\ No newline at end of file
diff --git a/apiserver/plane/api/serializers/project.py b/apiserver/plane/api/serializers/project.py
index 49d986cae0..932597799a 100644
--- a/apiserver/plane/api/serializers/project.py
+++ b/apiserver/plane/api/serializers/project.py
@@ -1,34 +1,60 @@
-# Django imports
-from django.db import IntegrityError
-
# Third party imports
from rest_framework import serializers
# Module imports
+from plane.db.models import Project, ProjectIdentifier, WorkspaceMember, State, Estimate
from .base import BaseSerializer
-from plane.api.serializers.workspace import WorkSpaceSerializer, WorkspaceLiteSerializer
-from plane.api.serializers.user import UserLiteSerializer, UserAdminLiteSerializer
-from plane.db.models import (
- Project,
- ProjectMember,
- ProjectMemberInvite,
- ProjectIdentifier,
- ProjectFavorite,
- ProjectDeployBoard,
- ProjectPublicMember,
-)
class ProjectSerializer(BaseSerializer):
- workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
+
+ total_members = serializers.IntegerField(read_only=True)
+ total_cycles = serializers.IntegerField(read_only=True)
+ total_modules = serializers.IntegerField(read_only=True)
+ is_member = serializers.BooleanField(read_only=True)
+ sort_order = serializers.FloatField(read_only=True)
+ member_role = serializers.IntegerField(read_only=True)
+ is_deployed = serializers.BooleanField(read_only=True)
class Meta:
model = Project
fields = "__all__"
read_only_fields = [
+ "id",
"workspace",
+ "created_at",
+ "updated_at",
+ "created_by",
+ "updated_by",
]
+ def validate(self, data):
+ # Check project lead should be a member of the workspace
+ if (
+ data.get("project_lead", None) is not None
+ and not WorkspaceMember.objects.filter(
+ workspace_id=self.context["workspace_id"],
+ member_id=data.get("project_lead"),
+ ).exists()
+ ):
+ raise serializers.ValidationError(
+ "Project lead should be a user in the workspace"
+ )
+
+ # Check default assignee should be a member of the workspace
+ if (
+ data.get("default_assignee", None) is not None
+ and not WorkspaceMember.objects.filter(
+ workspace_id=self.context["workspace_id"],
+ member_id=data.get("default_assignee"),
+ ).exists()
+ ):
+ raise serializers.ValidationError(
+ "Default assignee should be a user in the workspace"
+ )
+
+ return data
+
def create(self, validated_data):
identifier = validated_data.get("identifier", "").strip().upper()
if identifier == "":
@@ -38,6 +64,7 @@ class ProjectSerializer(BaseSerializer):
name=identifier, workspace_id=self.context["workspace_id"]
).exists():
raise serializers.ValidationError(detail="Project Identifier is taken")
+
project = Project.objects.create(
**validated_data, workspace_id=self.context["workspace_id"]
)
@@ -48,36 +75,6 @@ class ProjectSerializer(BaseSerializer):
)
return project
- def update(self, instance, validated_data):
- identifier = validated_data.get("identifier", "").strip().upper()
-
- # If identifier is not passed update the project and return
- if identifier == "":
- 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
- project = super().update(instance, validated_data)
- return project
-
- # If not same fail update
- raise serializers.ValidationError(detail="Project Identifier is already taken")
-
class ProjectLiteSerializer(BaseSerializer):
class Meta:
@@ -91,104 +88,4 @@ class ProjectLiteSerializer(BaseSerializer):
"emoji",
"description",
]
- read_only_fields = fields
-
-
-class ProjectDetailSerializer(BaseSerializer):
- workspace = WorkSpaceSerializer(read_only=True)
- default_assignee = UserLiteSerializer(read_only=True)
- project_lead = UserLiteSerializer(read_only=True)
- is_favorite = serializers.BooleanField(read_only=True)
- total_members = serializers.IntegerField(read_only=True)
- total_cycles = serializers.IntegerField(read_only=True)
- total_modules = serializers.IntegerField(read_only=True)
- is_member = serializers.BooleanField(read_only=True)
- sort_order = serializers.FloatField(read_only=True)
- member_role = serializers.IntegerField(read_only=True)
- is_deployed = serializers.BooleanField(read_only=True)
-
- class Meta:
- model = Project
- fields = "__all__"
-
-
-class ProjectMemberSerializer(BaseSerializer):
- workspace = WorkspaceLiteSerializer(read_only=True)
- project = ProjectLiteSerializer(read_only=True)
- member = UserLiteSerializer(read_only=True)
-
- class Meta:
- model = ProjectMember
- fields = "__all__"
-
-
-class ProjectMemberAdminSerializer(BaseSerializer):
- workspace = WorkspaceLiteSerializer(read_only=True)
- project = ProjectLiteSerializer(read_only=True)
- member = UserAdminLiteSerializer(read_only=True)
-
- class Meta:
- model = ProjectMember
- fields = "__all__"
-
-
-class ProjectMemberInviteSerializer(BaseSerializer):
- project = ProjectLiteSerializer(read_only=True)
- workspace = WorkspaceLiteSerializer(read_only=True)
-
- class Meta:
- model = ProjectMemberInvite
- fields = "__all__"
-
-
-class ProjectIdentifierSerializer(BaseSerializer):
- class Meta:
- model = ProjectIdentifier
- fields = "__all__"
-
-
-class ProjectFavoriteSerializer(BaseSerializer):
- project_detail = ProjectLiteSerializer(source="project", read_only=True)
-
- class Meta:
- model = ProjectFavorite
- fields = "__all__"
- read_only_fields = [
- "workspace",
- "user",
- ]
-
-
-class ProjectMemberLiteSerializer(BaseSerializer):
- member = UserLiteSerializer(read_only=True)
- is_subscribed = serializers.BooleanField(read_only=True)
-
- class Meta:
- model = ProjectMember
- fields = ["member", "id", "is_subscribed"]
- read_only_fields = fields
-
-
-class ProjectDeployBoardSerializer(BaseSerializer):
- project_details = ProjectLiteSerializer(read_only=True, source="project")
- workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
-
- class Meta:
- model = ProjectDeployBoard
- fields = "__all__"
- read_only_fields = [
- "workspace",
- "project", "anchor",
- ]
-
-
-class ProjectPublicMemberSerializer(BaseSerializer):
-
- class Meta:
- model = ProjectPublicMember
- fields = "__all__"
- read_only_fields = [
- "workspace",
- "project",
- "member",
- ]
+ read_only_fields = fields
\ No newline at end of file
diff --git a/apiserver/plane/api/serializers/state.py b/apiserver/plane/api/serializers/state.py
index 097bc4c931..4c7f05ab83 100644
--- a/apiserver/plane/api/serializers/state.py
+++ b/apiserver/plane/api/serializers/state.py
@@ -1,14 +1,16 @@
# Module imports
from .base import BaseSerializer
-from .workspace import WorkspaceLiteSerializer
-from .project import ProjectLiteSerializer
-
from plane.db.models import State
class StateSerializer(BaseSerializer):
- workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
- project_detail = ProjectLiteSerializer(read_only=True, source="project")
+ def validate(self, data):
+ # If the default is being provided then make all other states default False
+ if data.get("default", False):
+ State.objects.filter(project_id=self.context.get("project_id")).update(
+ default=False
+ )
+ return data
class Meta:
model = State
@@ -28,4 +30,4 @@ class StateLiteSerializer(BaseSerializer):
"color",
"group",
]
- read_only_fields = fields
+ read_only_fields = fields
\ No newline at end of file
diff --git a/apiserver/plane/api/serializers/user.py b/apiserver/plane/api/serializers/user.py
index dcb00c6cbf..e5a77da93d 100644
--- a/apiserver/plane/api/serializers/user.py
+++ b/apiserver/plane/api/serializers/user.py
@@ -1,36 +1,6 @@
-# Third party imports
-from rest_framework import serializers
-
-# Module import
-from .base import BaseSerializer
+# Module imports
from plane.db.models import User
-
-
-class UserSerializer(BaseSerializer):
- class Meta:
- model = User
- fields = "__all__"
- read_only_fields = [
- "id",
- "created_at",
- "updated_at",
- "is_superuser",
- "is_staff",
- "last_active",
- "last_login_time",
- "last_logout_time",
- "last_login_ip",
- "last_logout_ip",
- "last_login_uagent",
- "token_updated_at",
- "is_onboarded",
- "is_bot",
- ]
- extra_kwargs = {"password": {"write_only": True}}
-
- # If the user has already filled first name or last name then he is onboarded
- def get_is_onboarded(self, obj):
- return bool(obj.first_name) or bool(obj.last_name)
+from .base import BaseSerializer
class UserLiteSerializer(BaseSerializer):
@@ -47,43 +17,4 @@ class UserLiteSerializer(BaseSerializer):
read_only_fields = [
"id",
"is_bot",
- ]
-
-
-class UserAdminLiteSerializer(BaseSerializer):
-
- class Meta:
- model = User
- fields = [
- "id",
- "first_name",
- "last_name",
- "avatar",
- "is_bot",
- "display_name",
- "email",
- ]
- read_only_fields = [
- "id",
- "is_bot",
- ]
-
-
-class ChangePasswordSerializer(serializers.Serializer):
- model = User
-
- """
- Serializer for password change endpoint.
- """
- old_password = serializers.CharField(required=True)
- new_password = serializers.CharField(required=True)
-
-
-class ResetPasswordSerializer(serializers.Serializer):
- model = User
-
- """
- Serializer for password change endpoint.
- """
- new_password = serializers.CharField(required=True)
- confirm_password = serializers.CharField(required=True)
+ ]
\ No newline at end of file
diff --git a/apiserver/plane/api/serializers/workspace.py b/apiserver/plane/api/serializers/workspace.py
index d27b66481c..c4c5caceb3 100644
--- a/apiserver/plane/api/serializers/workspace.py
+++ b/apiserver/plane/api/serializers/workspace.py
@@ -1,39 +1,10 @@
-# Third party imports
-from rest_framework import serializers
-
# Module imports
+from plane.db.models import Workspace
from .base import BaseSerializer
-from .user import UserLiteSerializer, UserAdminLiteSerializer
-from plane.db.models import (
- User,
- Workspace,
- WorkspaceMember,
- Team,
- TeamMember,
- WorkspaceMemberInvite,
- WorkspaceTheme,
-)
-
-
-class WorkSpaceSerializer(BaseSerializer):
- owner = UserLiteSerializer(read_only=True)
- total_members = serializers.IntegerField(read_only=True)
- total_issues = serializers.IntegerField(read_only=True)
-
- class Meta:
- model = Workspace
- fields = "__all__"
- read_only_fields = [
- "id",
- "created_by",
- "updated_by",
- "created_at",
- "updated_at",
- "owner",
- ]
class WorkspaceLiteSerializer(BaseSerializer):
+ """Lite serializer with only required fields"""
class Meta:
model = Workspace
fields = [
@@ -41,91 +12,4 @@ class WorkspaceLiteSerializer(BaseSerializer):
"slug",
"id",
]
- read_only_fields = fields
-
-
-
-class WorkSpaceMemberSerializer(BaseSerializer):
- member = UserLiteSerializer(read_only=True)
- workspace = WorkspaceLiteSerializer(read_only=True)
-
- class Meta:
- model = WorkspaceMember
- fields = "__all__"
-
-
-class WorkspaceMemberAdminSerializer(BaseSerializer):
- member = UserAdminLiteSerializer(read_only=True)
- workspace = WorkspaceLiteSerializer(read_only=True)
-
- class Meta:
- model = WorkspaceMember
- fields = "__all__"
-
-
-class WorkSpaceMemberInviteSerializer(BaseSerializer):
- workspace = WorkSpaceSerializer(read_only=True)
- total_members = serializers.IntegerField(read_only=True)
- created_by_detail = UserLiteSerializer(read_only=True, source="created_by")
-
- class Meta:
- model = WorkspaceMemberInvite
- fields = "__all__"
-
-
-class TeamSerializer(BaseSerializer):
- members_detail = UserLiteSerializer(read_only=True, source="members", many=True)
- members = serializers.ListField(
- child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
- write_only=True,
- required=False,
- )
-
- class Meta:
- model = Team
- fields = "__all__"
- read_only_fields = [
- "workspace",
- "created_by",
- "updated_by",
- "created_at",
- "updated_at",
- ]
-
- def create(self, validated_data, **kwargs):
- if "members" in validated_data:
- members = validated_data.pop("members")
- workspace = self.context["workspace"]
- team = Team.objects.create(**validated_data, workspace=workspace)
- team_members = [
- TeamMember(member=member, team=team, workspace=workspace)
- for member in members
- ]
- TeamMember.objects.bulk_create(team_members, batch_size=10)
- return team
- else:
- team = Team.objects.create(**validated_data)
- return team
-
- def update(self, instance, validated_data):
- if "members" in validated_data:
- members = validated_data.pop("members")
- TeamMember.objects.filter(team=instance).delete()
- team_members = [
- TeamMember(member=member, team=instance, workspace=instance.workspace)
- for member in members
- ]
- TeamMember.objects.bulk_create(team_members, batch_size=10)
- return super().update(instance, validated_data)
- else:
- return super().update(instance, validated_data)
-
-
-class WorkspaceThemeSerializer(BaseSerializer):
- class Meta:
- model = WorkspaceTheme
- fields = "__all__"
- read_only_fields = [
- "workspace",
- "actor",
- ]
+ read_only_fields = fields
\ No newline at end of file
diff --git a/apiserver/plane/api/urls/__init__.py b/apiserver/plane/api/urls/__init__.py
new file mode 100644
index 0000000000..a5ef0f5f18
--- /dev/null
+++ b/apiserver/plane/api/urls/__init__.py
@@ -0,0 +1,15 @@
+from .project import urlpatterns as project_patterns
+from .state import urlpatterns as state_patterns
+from .issue import urlpatterns as issue_patterns
+from .cycle import urlpatterns as cycle_patterns
+from .module import urlpatterns as module_patterns
+from .inbox import urlpatterns as inbox_patterns
+
+urlpatterns = [
+ *project_patterns,
+ *state_patterns,
+ *issue_patterns,
+ *cycle_patterns,
+ *module_patterns,
+ *inbox_patterns,
+]
\ No newline at end of file
diff --git a/apiserver/plane/api/urls/cycle.py b/apiserver/plane/api/urls/cycle.py
new file mode 100644
index 0000000000..f557f8af0a
--- /dev/null
+++ b/apiserver/plane/api/urls/cycle.py
@@ -0,0 +1,35 @@
+from django.urls import path
+
+from plane.api.views.cycle import (
+ CycleAPIEndpoint,
+ CycleIssueAPIEndpoint,
+ TransferCycleIssueAPIEndpoint,
+)
+
+urlpatterns = [
+ path(
+ "workspaces//projects//cycles/",
+ CycleAPIEndpoint.as_view(),
+ name="cycles",
+ ),
+ path(
+ "workspaces//projects//cycles//",
+ CycleAPIEndpoint.as_view(),
+ name="cycles",
+ ),
+ path(
+ "workspaces//projects//cycles//cycle-issues/",
+ CycleIssueAPIEndpoint.as_view(),
+ name="cycle-issues",
+ ),
+ path(
+ "workspaces//projects//cycles//cycle-issues//",
+ CycleIssueAPIEndpoint.as_view(),
+ name="cycle-issues",
+ ),
+ path(
+ "workspaces//projects//cycles//transfer-issues/",
+ TransferCycleIssueAPIEndpoint.as_view(),
+ name="transfer-issues",
+ ),
+]
\ No newline at end of file
diff --git a/apiserver/plane/api/urls/inbox.py b/apiserver/plane/api/urls/inbox.py
new file mode 100644
index 0000000000..3a2a57786a
--- /dev/null
+++ b/apiserver/plane/api/urls/inbox.py
@@ -0,0 +1,17 @@
+from django.urls import path
+
+from plane.api.views import InboxIssueAPIEndpoint
+
+
+urlpatterns = [
+ path(
+ "workspaces//projects//inbox-issues/",
+ InboxIssueAPIEndpoint.as_view(),
+ name="inbox-issue",
+ ),
+ path(
+ "workspaces//projects//inbox-issues//",
+ InboxIssueAPIEndpoint.as_view(),
+ name="inbox-issue",
+ ),
+]
\ No newline at end of file
diff --git a/apiserver/plane/api/urls/issue.py b/apiserver/plane/api/urls/issue.py
new file mode 100644
index 0000000000..070ea8bd9c
--- /dev/null
+++ b/apiserver/plane/api/urls/issue.py
@@ -0,0 +1,62 @@
+from django.urls import path
+
+from plane.api.views import (
+ IssueAPIEndpoint,
+ LabelAPIEndpoint,
+ IssueLinkAPIEndpoint,
+ IssueCommentAPIEndpoint,
+ IssueActivityAPIEndpoint,
+)
+
+urlpatterns = [
+ path(
+ "workspaces//projects//issues/",
+ IssueAPIEndpoint.as_view(),
+ name="issue",
+ ),
+ path(
+ "workspaces//projects//issues//",
+ IssueAPIEndpoint.as_view(),
+ name="issue",
+ ),
+ path(
+ "workspaces//projects//labels/",
+ LabelAPIEndpoint.as_view(),
+ name="label",
+ ),
+ path(
+ "workspaces//projects//labels//",
+ LabelAPIEndpoint.as_view(),
+ name="label",
+ ),
+ path(
+ "workspaces//projects//issues//links/",
+ IssueLinkAPIEndpoint.as_view(),
+ name="link",
+ ),
+ path(
+ "workspaces//projects//issues//links//",
+ IssueLinkAPIEndpoint.as_view(),
+ name="link",
+ ),
+ path(
+ "workspaces//projects//issues//comments/",
+ IssueCommentAPIEndpoint.as_view(),
+ name="comment",
+ ),
+ path(
+ "workspaces//projects//issues//comments//",
+ IssueCommentAPIEndpoint.as_view(),
+ name="comment",
+ ),
+ path(
+ "workspaces//projects//issues//activities/",
+ IssueActivityAPIEndpoint.as_view(),
+ name="activity",
+ ),
+ path(
+ "workspaces//projects//issues//activities//",
+ IssueActivityAPIEndpoint.as_view(),
+ name="activity",
+ ),
+]
diff --git a/apiserver/plane/api/urls/module.py b/apiserver/plane/api/urls/module.py
new file mode 100644
index 0000000000..7117a9e8b8
--- /dev/null
+++ b/apiserver/plane/api/urls/module.py
@@ -0,0 +1,26 @@
+from django.urls import path
+
+from plane.api.views import ModuleAPIEndpoint, ModuleIssueAPIEndpoint
+
+urlpatterns = [
+ path(
+ "workspaces//projects//modules/",
+ ModuleAPIEndpoint.as_view(),
+ name="modules",
+ ),
+ path(
+ "workspaces//projects//modules//",
+ ModuleAPIEndpoint.as_view(),
+ name="modules",
+ ),
+ path(
+ "workspaces//projects//modules//module-issues/",
+ ModuleIssueAPIEndpoint.as_view(),
+ name="module-issues",
+ ),
+ path(
+ "workspaces//projects//modules//module-issues//",
+ ModuleIssueAPIEndpoint.as_view(),
+ name="module-issues",
+ ),
+]
\ No newline at end of file
diff --git a/apiserver/plane/api/urls/project.py b/apiserver/plane/api/urls/project.py
new file mode 100644
index 0000000000..c73e84c89d
--- /dev/null
+++ b/apiserver/plane/api/urls/project.py
@@ -0,0 +1,16 @@
+from django.urls import path
+
+from plane.api.views import ProjectAPIEndpoint
+
+urlpatterns = [
+ path(
+ "workspaces//projects/",
+ ProjectAPIEndpoint.as_view(),
+ name="project",
+ ),
+ path(
+ "workspaces//projects//",
+ ProjectAPIEndpoint.as_view(),
+ name="project",
+ ),
+]
\ No newline at end of file
diff --git a/apiserver/plane/api/urls/state.py b/apiserver/plane/api/urls/state.py
new file mode 100644
index 0000000000..0676ac5ade
--- /dev/null
+++ b/apiserver/plane/api/urls/state.py
@@ -0,0 +1,16 @@
+from django.urls import path
+
+from plane.api.views import StateAPIEndpoint
+
+urlpatterns = [
+ path(
+ "workspaces//projects//states/",
+ StateAPIEndpoint.as_view(),
+ name="states",
+ ),
+ path(
+ "workspaces//projects//states//",
+ StateAPIEndpoint.as_view(),
+ name="states",
+ ),
+]
\ No newline at end of file
diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py
index f7ad735c11..84d8dcabb1 100644
--- a/apiserver/plane/api/views/__init__.py
+++ b/apiserver/plane/api/views/__init__.py
@@ -1,172 +1,21 @@
-from .project import (
- ProjectViewSet,
- ProjectMemberViewSet,
- UserProjectInvitationsViewset,
- InviteProjectEndpoint,
- AddTeamToProjectEndpoint,
- ProjectMemberInvitationsViewset,
- ProjectMemberInviteDetailViewSet,
- ProjectIdentifierEndpoint,
- AddMemberToProjectEndpoint,
- ProjectJoinEndpoint,
- ProjectUserViewsEndpoint,
- ProjectMemberUserEndpoint,
- ProjectFavoritesViewSet,
- ProjectDeployBoardViewSet,
- ProjectDeployBoardPublicSettingsEndpoint,
- ProjectMemberEndpoint,
- WorkspaceProjectDeployBoardEndpoint,
- LeaveProjectEndpoint,
- ProjectPublicCoverImagesEndpoint,
-)
-from .user import (
- UserEndpoint,
- UpdateUserOnBoardedEndpoint,
- UpdateUserTourCompletedEndpoint,
- UserActivityEndpoint,
-)
+from .project import ProjectAPIEndpoint
-from .oauth import OauthEndpoint
+from .state import StateAPIEndpoint
-from .base import BaseAPIView, BaseViewSet
-
-from .workspace import (
- WorkSpaceViewSet,
- UserWorkSpacesEndpoint,
- WorkSpaceAvailabilityCheckEndpoint,
- InviteWorkspaceEndpoint,
- JoinWorkspaceEndpoint,
- WorkSpaceMemberViewSet,
- TeamMemberViewSet,
- WorkspaceInvitationsViewset,
- UserWorkspaceInvitationsEndpoint,
- UserWorkspaceInvitationEndpoint,
- UserLastProjectWithWorkspaceEndpoint,
- WorkspaceMemberUserEndpoint,
- WorkspaceMemberUserViewsEndpoint,
- UserActivityGraphEndpoint,
- UserIssueCompletedGraphEndpoint,
- UserWorkspaceDashboardEndpoint,
- WorkspaceThemeViewSet,
- WorkspaceUserProfileStatsEndpoint,
- WorkspaceUserActivityEndpoint,
- WorkspaceUserProfileEndpoint,
- WorkspaceUserProfileIssuesEndpoint,
- WorkspaceLabelsEndpoint,
- WorkspaceMembersEndpoint,
- LeaveWorkspaceEndpoint,
-)
-from .state import StateViewSet
-from .view import GlobalViewViewSet, GlobalViewIssuesViewSet, IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet
-from .cycle import (
- CycleViewSet,
- CycleIssueViewSet,
- CycleDateCheckEndpoint,
- CycleFavoriteViewSet,
- TransferCycleIssueEndpoint,
-)
-from .asset import FileAssetEndpoint, UserAssetsEndpoint
from .issue import (
- IssueViewSet,
- WorkSpaceIssuesEndpoint,
- IssueActivityEndpoint,
- IssueCommentViewSet,
- IssuePropertyViewSet,
- LabelViewSet,
- BulkDeleteIssuesEndpoint,
- UserWorkSpaceIssues,
- SubIssuesEndpoint,
- IssueLinkViewSet,
- BulkCreateIssueLabelsEndpoint,
- IssueAttachmentEndpoint,
- IssueArchiveViewSet,
- IssueSubscriberViewSet,
- IssueCommentPublicViewSet,
- CommentReactionViewSet,
- IssueReactionViewSet,
- IssueReactionPublicViewSet,
- CommentReactionPublicViewSet,
- IssueVotePublicViewSet,
- IssueRelationViewSet,
- IssueRetrievePublicEndpoint,
- ProjectIssuesPublicEndpoint,
- IssueDraftViewSet,
+ IssueAPIEndpoint,
+ LabelAPIEndpoint,
+ IssueLinkAPIEndpoint,
+ IssueCommentAPIEndpoint,
+ IssueActivityAPIEndpoint,
)
-from .auth_extended import (
- VerifyEmailEndpoint,
- RequestEmailVerificationEndpoint,
- ForgotPasswordEndpoint,
- ResetPasswordEndpoint,
- ChangePasswordEndpoint,
+from .cycle import (
+ CycleAPIEndpoint,
+ CycleIssueAPIEndpoint,
+ TransferCycleIssueAPIEndpoint,
)
+from .module import ModuleAPIEndpoint, ModuleIssueAPIEndpoint
-from .authentication import (
- SignUpEndpoint,
- SignInEndpoint,
- SignOutEndpoint,
- MagicSignInEndpoint,
- MagicSignInGenerateEndpoint,
-)
-
-from .module import (
- ModuleViewSet,
- ModuleIssueViewSet,
- ModuleLinkViewSet,
- ModuleFavoriteViewSet,
-)
-
-from .api_token import ApiTokenEndpoint
-
-from .integration import (
- WorkspaceIntegrationViewSet,
- IntegrationViewSet,
- GithubIssueSyncViewSet,
- GithubRepositorySyncViewSet,
- GithubCommentSyncViewSet,
- GithubRepositoriesEndpoint,
- BulkCreateGithubIssueSyncEndpoint,
- SlackProjectSyncViewSet,
-)
-
-from .importer import (
- ServiceIssueImportSummaryEndpoint,
- ImportServiceEndpoint,
- UpdateServiceImportStatusEndpoint,
- BulkImportIssuesEndpoint,
- BulkImportModulesEndpoint,
-)
-
-from .page import (
- PageViewSet,
- PageBlockViewSet,
- PageFavoriteViewSet,
- CreateIssueFromPageBlockEndpoint,
-)
-
-from .search import GlobalSearchEndpoint, IssueSearchEndpoint
-
-
-from .external import GPTIntegrationEndpoint, ReleaseNotesEndpoint, UnsplashEndpoint
-
-from .estimate import (
- ProjectEstimatePointEndpoint,
- BulkEstimatePointEndpoint,
-)
-
-from .inbox import InboxViewSet, InboxIssueViewSet, InboxIssuePublicViewSet
-
-from .analytic import (
- AnalyticsEndpoint,
- AnalyticViewViewset,
- SavedAnalyticEndpoint,
- ExportAnalyticsEndpoint,
- DefaultAnalyticsEndpoint,
-)
-
-from .notification import NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet
-
-from .exporter import ExportIssuesEndpoint
-
-from .config import ConfigurationEndpoint
\ No newline at end of file
+from .inbox import InboxIssueAPIEndpoint
\ No newline at end of file
diff --git a/apiserver/plane/api/views/analytic.py b/apiserver/plane/api/views/analytic.py
deleted file mode 100644
index feb766b46d..0000000000
--- a/apiserver/plane/api/views/analytic.py
+++ /dev/null
@@ -1,297 +0,0 @@
-# Django imports
-from django.db.models import (
- Count,
- Sum,
- F,
- Q
-)
-from django.db.models.functions import ExtractMonth
-
-# Third party imports
-from rest_framework import status
-from rest_framework.response import Response
-from sentry_sdk import capture_exception
-
-# Module imports
-from plane.api.views import BaseAPIView, BaseViewSet
-from plane.api.permissions import WorkSpaceAdminPermission
-from plane.db.models import Issue, AnalyticView, Workspace, State, Label
-from plane.api.serializers import AnalyticViewSerializer
-from plane.utils.analytics_plot import build_graph_plot
-from plane.bgtasks.analytic_plot_export import analytic_export_task
-from plane.utils.issue_filters import issue_filters
-
-
-class AnalyticsEndpoint(BaseAPIView):
- permission_classes = [
- WorkSpaceAdminPermission,
- ]
-
- def get(self, request, slug):
- try:
- x_axis = request.GET.get("x_axis", False)
- y_axis = request.GET.get("y_axis", False)
-
- if not x_axis or not y_axis:
- return Response(
- {"error": "x-axis and y-axis dimensions are required"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- segment = request.GET.get("segment", False)
- filters = issue_filters(request.GET, "GET")
-
- queryset = Issue.issue_objects.filter(workspace__slug=slug, **filters)
-
- total_issues = queryset.count()
- distribution = build_graph_plot(
- queryset=queryset, x_axis=x_axis, y_axis=y_axis, segment=segment
- )
-
- colors = dict()
- if x_axis in ["state__name", "state__group"] or segment in [
- "state__name",
- "state__group",
- ]:
- if x_axis in ["state__name", "state__group"]:
- key = "name" if x_axis == "state__name" else "group"
- else:
- key = "name" if segment == "state__name" else "group"
-
- colors = (
- State.objects.filter(
- ~Q(name="Triage"),
- workspace__slug=slug, project_id__in=filters.get("project__in")
- ).values(key, "color")
- if filters.get("project__in", False)
- else State.objects.filter(~Q(name="Triage"), workspace__slug=slug).values(key, "color")
- )
-
- if x_axis in ["labels__name"] or segment in ["labels__name"]:
- colors = (
- Label.objects.filter(
- workspace__slug=slug, project_id__in=filters.get("project__in")
- ).values("name", "color")
- if filters.get("project__in", False)
- else Label.objects.filter(workspace__slug=slug).values(
- "name", "color"
- )
- )
-
- assignee_details = {}
- if x_axis in ["assignees__id"] or segment in ["assignees__id"]:
- assignee_details = (
- Issue.issue_objects.filter(workspace__slug=slug, **filters, assignees__avatar__isnull=False)
- .order_by("assignees__id")
- .distinct("assignees__id")
- .values("assignees__avatar", "assignees__display_name", "assignees__first_name", "assignees__last_name", "assignees__id")
- )
-
-
- return Response(
- {
- "total": total_issues,
- "distribution": distribution,
- "extras": {"colors": colors, "assignee_details": assignee_details},
- },
- status=status.HTTP_200_OK,
- )
-
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class AnalyticViewViewset(BaseViewSet):
- permission_classes = [
- WorkSpaceAdminPermission,
- ]
- model = AnalyticView
- serializer_class = AnalyticViewSerializer
-
- def perform_create(self, serializer):
- workspace = Workspace.objects.get(slug=self.kwargs.get("slug"))
- serializer.save(workspace_id=workspace.id)
-
- def get_queryset(self):
- return self.filter_queryset(
- super().get_queryset().filter(workspace__slug=self.kwargs.get("slug"))
- )
-
-
-class SavedAnalyticEndpoint(BaseAPIView):
- permission_classes = [
- WorkSpaceAdminPermission,
- ]
-
- def get(self, request, slug, analytic_id):
- try:
- analytic_view = AnalyticView.objects.get(
- pk=analytic_id, workspace__slug=slug
- )
-
- filter = analytic_view.query
- queryset = Issue.issue_objects.filter(**filter)
-
- x_axis = analytic_view.query_dict.get("x_axis", False)
- y_axis = analytic_view.query_dict.get("y_axis", False)
-
- if not x_axis or not y_axis:
- return Response(
- {"error": "x-axis and y-axis dimensions are required"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- segment = request.GET.get("segment", False)
- distribution = build_graph_plot(
- queryset=queryset, x_axis=x_axis, y_axis=y_axis, segment=segment
- )
- total_issues = queryset.count()
- return Response(
- {"total": total_issues, "distribution": distribution},
- status=status.HTTP_200_OK,
- )
-
- except AnalyticView.DoesNotExist:
- return Response(
- {"error": "Analytic View Does not exist"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class ExportAnalyticsEndpoint(BaseAPIView):
- permission_classes = [
- WorkSpaceAdminPermission,
- ]
-
- def post(self, request, slug):
- try:
- x_axis = request.data.get("x_axis", False)
- y_axis = request.data.get("y_axis", False)
-
- if not x_axis or not y_axis:
- return Response(
- {"error": "x-axis and y-axis dimensions are required"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- analytic_export_task.delay(
- email=request.user.email, data=request.data, slug=slug
- )
-
- return Response(
- {
- "message": f"Once the export is ready it will be emailed to you at {str(request.user.email)}"
- },
- status=status.HTTP_200_OK,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class DefaultAnalyticsEndpoint(BaseAPIView):
- permission_classes = [
- WorkSpaceAdminPermission,
- ]
-
- def get(self, request, slug):
- try:
- filters = issue_filters(request.GET, "GET")
-
- queryset = Issue.issue_objects.filter(workspace__slug=slug, **filters)
-
- total_issues = queryset.count()
-
- total_issues_classified = (
- queryset.annotate(state_group=F("state__group"))
- .values("state_group")
- .annotate(state_count=Count("state_group"))
- .order_by("state_group")
- )
-
- open_issues = queryset.filter(
- state__group__in=["backlog", "unstarted", "started"]
- ).count()
-
- open_issues_classified = (
- queryset.filter(state__group__in=["backlog", "unstarted", "started"])
- .annotate(state_group=F("state__group"))
- .values("state_group")
- .annotate(state_count=Count("state_group"))
- .order_by("state_group")
- )
-
- issue_completed_month_wise = (
- queryset.filter(completed_at__isnull=False)
- .annotate(month=ExtractMonth("completed_at"))
- .values("month")
- .annotate(count=Count("*"))
- .order_by("month")
- )
- most_issue_created_user = (
- queryset.exclude(created_by=None)
- .values("created_by__first_name", "created_by__last_name", "created_by__avatar", "created_by__display_name", "created_by__id")
- .annotate(count=Count("id"))
- .order_by("-count")
- )[:5]
-
- most_issue_closed_user = (
- queryset.filter(completed_at__isnull=False, assignees__isnull=False)
- .values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__display_name", "assignees__id")
- .annotate(count=Count("id"))
- .order_by("-count")
- )[:5]
-
- pending_issue_user = (
- queryset.filter(completed_at__isnull=True)
- .values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__display_name", "assignees__id")
- .annotate(count=Count("id"))
- .order_by("-count")
- )
-
- open_estimate_sum = (
- queryset.filter(
- state__group__in=["backlog", "unstarted", "started"]
- ).aggregate(open_estimate_sum=Sum("estimate_point"))
- )["open_estimate_sum"]
- print(open_estimate_sum)
-
- total_estimate_sum = queryset.aggregate(
- total_estimate_sum=Sum("estimate_point")
- )["total_estimate_sum"]
-
- return Response(
- {
- "total_issues": total_issues,
- "total_issues_classified": total_issues_classified,
- "open_issues": open_issues,
- "open_issues_classified": open_issues_classified,
- "issue_completed_month_wise": issue_completed_month_wise,
- "most_issue_created_user": most_issue_created_user,
- "most_issue_closed_user": most_issue_closed_user,
- "pending_issue_user": pending_issue_user,
- "open_estimate_sum": open_estimate_sum,
- "total_estimate_sum": total_estimate_sum,
- },
- status=status.HTTP_200_OK,
- )
-
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
diff --git a/apiserver/plane/api/views/api_token.py b/apiserver/plane/api/views/api_token.py
deleted file mode 100644
index a94ffb45ca..0000000000
--- a/apiserver/plane/api/views/api_token.py
+++ /dev/null
@@ -1,70 +0,0 @@
-# Python import
-from uuid import uuid4
-
-# Third party
-from rest_framework.response import Response
-from rest_framework import status
-from sentry_sdk import capture_exception
-
-# Module import
-from .base import BaseAPIView
-from plane.db.models import APIToken
-from plane.api.serializers import APITokenSerializer
-
-
-class ApiTokenEndpoint(BaseAPIView):
- def post(self, request):
- try:
- label = request.data.get("label", str(uuid4().hex))
- workspace = request.data.get("workspace", False)
-
- if not workspace:
- return Response(
- {"error": "Workspace is required"}, status=status.HTTP_200_OK
- )
-
- api_token = APIToken.objects.create(
- label=label, user=request.user, workspace_id=workspace
- )
-
- serializer = APITokenSerializer(api_token)
- # Token will be only vissible while creating
- return Response(
- {"api_token": serializer.data, "token": api_token.token},
- status=status.HTTP_201_CREATED,
- )
-
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def get(self, request):
- try:
- api_tokens = APIToken.objects.filter(user=request.user)
- serializer = APITokenSerializer(api_tokens, many=True)
- return Response(serializer.data, status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def delete(self, request, pk):
- try:
- api_token = APIToken.objects.get(pk=pk)
- api_token.delete()
- return Response(status=status.HTTP_204_NO_CONTENT)
- except APIToken.DoesNotExist:
- return Response(
- {"error": "Token does not exists"}, status=status.HTTP_400_BAD_REQUEST
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
diff --git a/apiserver/plane/api/views/asset.py b/apiserver/plane/api/views/asset.py
deleted file mode 100644
index d9b6e502d1..0000000000
--- a/apiserver/plane/api/views/asset.py
+++ /dev/null
@@ -1,125 +0,0 @@
-# Third party imports
-from rest_framework import status
-from rest_framework.response import Response
-from rest_framework.parsers import MultiPartParser, FormParser
-from sentry_sdk import capture_exception
-from django.conf import settings
-# Module imports
-from .base import BaseAPIView
-from plane.db.models import FileAsset, Workspace
-from plane.api.serializers import FileAssetSerializer
-
-
-class FileAssetEndpoint(BaseAPIView):
- parser_classes = (MultiPartParser, FormParser)
-
- """
- A viewset for viewing and editing task instances.
- """
-
- def get(self, request, workspace_id, asset_key):
- try:
- asset_key = str(workspace_id) + "/" + asset_key
- files = FileAsset.objects.filter(asset=asset_key)
- if files.exists():
- serializer = FileAssetSerializer(files, context={"request": request}, many=True)
- return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK)
- else:
- return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
- def post(self, request, slug):
- try:
- serializer = FileAssetSerializer(data=request.data)
- if serializer.is_valid():
- # Get the workspace
- workspace = Workspace.objects.get(slug=slug)
- serializer.save(workspace_id=workspace.id)
- return Response(serializer.data, status=status.HTTP_201_CREATED)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- except Workspace.DoesNotExist:
- return Response({"error": "Workspace does not exist"}, status=status.HTTP_400_BAD_REQUEST)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def delete(self, request, workspace_id, asset_key):
- try:
- asset_key = str(workspace_id) + "/" + asset_key
- file_asset = FileAsset.objects.get(asset=asset_key)
- # Delete the file from storage
- file_asset.asset.delete(save=False)
- # Delete the file object
- file_asset.delete()
- return Response(status=status.HTTP_204_NO_CONTENT)
- except FileAsset.DoesNotExist:
- return Response(
- {"error": "File Asset doesn't exist"}, status=status.HTTP_404_NOT_FOUND
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class UserAssetsEndpoint(BaseAPIView):
- parser_classes = (MultiPartParser, FormParser)
-
- def get(self, request, asset_key):
- try:
- files = FileAsset.objects.filter(asset=asset_key, created_by=request.user)
- if files.exists():
- serializer = FileAssetSerializer(files, context={"request": request})
- return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK)
- else:
- return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def post(self, request):
- try:
- serializer = FileAssetSerializer(data=request.data)
- if serializer.is_valid():
- serializer.save()
- return Response(serializer.data, status=status.HTTP_201_CREATED)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def delete(self, request, asset_key):
- try:
- file_asset = FileAsset.objects.get(asset=asset_key, created_by=request.user)
- # Delete the file from storage
- file_asset.asset.delete(save=False)
- # Delete the file object
- file_asset.delete()
- return Response(status=status.HTTP_204_NO_CONTENT)
- except FileAsset.DoesNotExist:
- return Response(
- {"error": "File Asset doesn't exist"}, status=status.HTTP_404_NOT_FOUND
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
diff --git a/apiserver/plane/api/views/auth_extended.py b/apiserver/plane/api/views/auth_extended.py
deleted file mode 100644
index df3f3aaca8..0000000000
--- a/apiserver/plane/api/views/auth_extended.py
+++ /dev/null
@@ -1,159 +0,0 @@
-## Python imports
-import jwt
-
-## Django imports
-from django.contrib.auth.tokens import PasswordResetTokenGenerator
-from django.utils.encoding import (
- smart_str,
- smart_bytes,
- DjangoUnicodeDecodeError,
-)
-from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
-from django.contrib.sites.shortcuts import get_current_site
-from django.conf import settings
-
-## Third Party Imports
-from rest_framework import status
-from rest_framework.response import Response
-from rest_framework import permissions
-from rest_framework_simplejwt.tokens import RefreshToken
-
-from sentry_sdk import capture_exception
-
-## Module imports
-from . import BaseAPIView
-from plane.api.serializers import (
- ChangePasswordSerializer,
- ResetPasswordSerializer,
-)
-from plane.db.models import User
-from plane.bgtasks.email_verification_task import email_verification
-from plane.bgtasks.forgot_password_task import forgot_password
-
-
-class RequestEmailVerificationEndpoint(BaseAPIView):
- def get(self, request):
- token = RefreshToken.for_user(request.user).access_token
- current_site = settings.WEB_URL
- email_verification.delay(
- request.user.first_name, request.user.email, token, current_site
- )
- return Response(
- {"message": "Email sent successfully"}, status=status.HTTP_200_OK
- )
-
-
-class VerifyEmailEndpoint(BaseAPIView):
- def get(self, request):
- token = request.GET.get("token")
- try:
- payload = jwt.decode(token, settings.SECRET_KEY, algorithms="HS256")
- user = User.objects.get(id=payload["user_id"])
-
- if not user.is_email_verified:
- user.is_email_verified = True
- user.save()
- return Response(
- {"email": "Successfully activated"}, status=status.HTTP_200_OK
- )
- except jwt.ExpiredSignatureError as indentifier:
- return Response(
- {"email": "Activation expired"}, status=status.HTTP_400_BAD_REQUEST
- )
- except jwt.exceptions.DecodeError as indentifier:
- return Response(
- {"email": "Invalid token"}, status=status.HTTP_400_BAD_REQUEST
- )
-
-
-class ForgotPasswordEndpoint(BaseAPIView):
- permission_classes = [permissions.AllowAny]
-
- def post(self, request):
- email = request.data.get("email")
-
- if User.objects.filter(email=email).exists():
- user = User.objects.get(email=email)
- uidb64 = urlsafe_base64_encode(smart_bytes(user.id))
- token = PasswordResetTokenGenerator().make_token(user)
-
- current_site = settings.WEB_URL
-
- forgot_password.delay(
- user.first_name, user.email, uidb64, token, current_site
- )
-
- return Response(
- {"message": "Check your email to reset your password"},
- status=status.HTTP_200_OK,
- )
- return Response(
- {"error": "Please check the email"}, status=status.HTTP_400_BAD_REQUEST
- )
-
-
-class ResetPasswordEndpoint(BaseAPIView):
- permission_classes = [permissions.AllowAny]
-
- def post(self, request, uidb64, token):
- try:
- id = smart_str(urlsafe_base64_decode(uidb64))
- user = User.objects.get(id=id)
- if not PasswordResetTokenGenerator().check_token(user, token):
- return Response(
- {"error": "token is not valid, please check the new one"},
- status=status.HTTP_401_UNAUTHORIZED,
- )
- serializer = ResetPasswordSerializer(data=request.data)
-
- if serializer.is_valid():
- # set_password also hashes the password that the user will get
- user.set_password(serializer.data.get("new_password"))
- user.save()
- response = {
- "status": "success",
- "code": status.HTTP_200_OK,
- "message": "Password updated successfully",
- }
-
- return Response(response)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-
- except DjangoUnicodeDecodeError as indentifier:
- return Response(
- {"error": "token is not valid, please check the new one"},
- status=status.HTTP_401_UNAUTHORIZED,
- )
-
-
-class ChangePasswordEndpoint(BaseAPIView):
- def post(self, request):
- try:
- serializer = ChangePasswordSerializer(data=request.data)
-
- user = User.objects.get(pk=request.user.id)
- if serializer.is_valid():
- # Check old password
- if not user.object.check_password(serializer.data.get("old_password")):
- return Response(
- {"old_password": ["Wrong password."]},
- status=status.HTTP_400_BAD_REQUEST,
- )
- # set_password also hashes the password that the user will get
- self.object.set_password(serializer.data.get("new_password"))
- self.object.save()
- response = {
- "status": "success",
- "code": status.HTTP_200_OK,
- "message": "Password updated successfully",
- }
-
- return Response(response)
-
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
diff --git a/apiserver/plane/api/views/authentication.py b/apiserver/plane/api/views/authentication.py
deleted file mode 100644
index aa8ff45116..0000000000
--- a/apiserver/plane/api/views/authentication.py
+++ /dev/null
@@ -1,458 +0,0 @@
-# Python imports
-import uuid
-import random
-import string
-import json
-import requests
-
-# Django imports
-from django.utils import timezone
-from django.core.exceptions import ValidationError
-from django.core.validators import validate_email
-from django.conf import settings
-from django.contrib.auth.hashers import make_password
-
-# Third party imports
-from rest_framework.response import Response
-from rest_framework.permissions import AllowAny
-from rest_framework import status
-from rest_framework_simplejwt.tokens import RefreshToken
-
-from sentry_sdk import capture_exception, capture_message
-
-# Module imports
-from . import BaseAPIView
-from plane.db.models import User
-from plane.api.serializers import UserSerializer
-from plane.settings.redis import redis_instance
-from plane.bgtasks.magic_link_code_task import magic_link
-
-
-def get_tokens_for_user(user):
- refresh = RefreshToken.for_user(user)
- return (
- str(refresh.access_token),
- str(refresh),
- )
-
-
-class SignUpEndpoint(BaseAPIView):
- permission_classes = (AllowAny,)
-
- def post(self, request):
- try:
- if not settings.ENABLE_SIGNUP:
- return Response(
- {
- "error": "New account creation is disabled. Please contact your site administrator"
- },
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- email = request.data.get("email", False)
- password = request.data.get("password", False)
-
- ## Raise exception if any of the above are missing
- if not email or not password:
- return Response(
- {"error": "Both email and password are required"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- email = email.strip().lower()
-
- try:
- validate_email(email)
- except ValidationError as e:
- return Response(
- {"error": "Please provide a valid email address."},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- # Check if the user already exists
- if User.objects.filter(email=email).exists():
- return Response(
- {"error": "User with this email already exists"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- user = User.objects.create(email=email, username=uuid.uuid4().hex)
- user.set_password(password)
-
- # settings last actives for the user
- user.last_active = timezone.now()
- user.last_login_time = timezone.now()
- user.last_login_ip = request.META.get("REMOTE_ADDR")
- user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
- user.token_updated_at = timezone.now()
- user.save()
-
- serialized_user = UserSerializer(user).data
-
- access_token, refresh_token = get_tokens_for_user(user)
-
- data = {
- "access_token": access_token,
- "refresh_token": refresh_token,
- "user": serialized_user,
- }
-
- # Send Analytics
- if settings.ANALYTICS_BASE_API:
- _ = requests.post(
- settings.ANALYTICS_BASE_API,
- headers={
- "Content-Type": "application/json",
- "X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
- },
- json={
- "event_id": uuid.uuid4().hex,
- "event_data": {
- "medium": "email",
- },
- "user": {"email": email, "id": str(user.id)},
- "device_ctx": {
- "ip": request.META.get("REMOTE_ADDR"),
- "user_agent": request.META.get("HTTP_USER_AGENT"),
- },
- "event_type": "SIGN_UP",
- },
- )
-
- return Response(data, status=status.HTTP_200_OK)
-
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class SignInEndpoint(BaseAPIView):
- permission_classes = (AllowAny,)
-
- def post(self, request):
- try:
- email = request.data.get("email", False)
- password = request.data.get("password", False)
-
- ## Raise exception if any of the above are missing
- if not email or not password:
- return Response(
- {"error": "Both email and password are required"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- email = email.strip().lower()
-
- try:
- validate_email(email)
- except ValidationError as e:
- return Response(
- {"error": "Please provide a valid email address."},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- user = User.objects.filter(email=email).first()
-
- if user is None:
- return Response(
- {
- "error": "Sorry, we could not find a user with the provided credentials. Please try again."
- },
- status=status.HTTP_403_FORBIDDEN,
- )
-
- # Sign up Process
- if not user.check_password(password):
- return Response(
- {
- "error": "Sorry, we could not find a user with the provided credentials. Please try again."
- },
- status=status.HTTP_403_FORBIDDEN,
- )
- if not user.is_active:
- return Response(
- {
- "error": "Your account has been deactivated. Please contact your site administrator."
- },
- status=status.HTTP_403_FORBIDDEN,
- )
-
- serialized_user = UserSerializer(user).data
-
- # settings last active for the user
- user.last_active = timezone.now()
- user.last_login_time = timezone.now()
- user.last_login_ip = request.META.get("REMOTE_ADDR")
- user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
- user.token_updated_at = timezone.now()
- user.save()
-
- access_token, refresh_token = get_tokens_for_user(user)
- # Send Analytics
- if settings.ANALYTICS_BASE_API:
- _ = requests.post(
- settings.ANALYTICS_BASE_API,
- headers={
- "Content-Type": "application/json",
- "X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
- },
- json={
- "event_id": uuid.uuid4().hex,
- "event_data": {
- "medium": "email",
- },
- "user": {"email": email, "id": str(user.id)},
- "device_ctx": {
- "ip": request.META.get("REMOTE_ADDR"),
- "user_agent": request.META.get("HTTP_USER_AGENT"),
- },
- "event_type": "SIGN_IN",
- },
- )
- data = {
- "access_token": access_token,
- "refresh_token": refresh_token,
- "user": serialized_user,
- }
-
- return Response(data, status=status.HTTP_200_OK)
-
- except Exception as e:
- capture_exception(e)
- return Response(
- {
- "error": "Something went wrong. Please try again later or contact the support team."
- },
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class SignOutEndpoint(BaseAPIView):
- def post(self, request):
- try:
- refresh_token = request.data.get("refresh_token", False)
-
- if not refresh_token:
- capture_message("No refresh token provided")
- return Response(
- {
- "error": "Something went wrong. Please try again later or contact the support team."
- },
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- user = User.objects.get(pk=request.user.id)
-
- user.last_logout_time = timezone.now()
- user.last_logout_ip = request.META.get("REMOTE_ADDR")
-
- user.save()
-
- token = RefreshToken(refresh_token)
- token.blacklist()
- return Response({"message": "success"}, status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
- return Response(
- {
- "error": "Something went wrong. Please try again later or contact the support team."
- },
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class MagicSignInGenerateEndpoint(BaseAPIView):
- permission_classes = [
- AllowAny,
- ]
-
- def post(self, request):
- try:
- email = request.data.get("email", False)
-
- if not email:
- return Response(
- {"error": "Please provide a valid email address"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- # Clean up
- email = email.strip().lower()
- validate_email(email)
-
- ## Generate a random token
- token = (
- "".join(random.choices(string.ascii_lowercase + string.digits, k=4))
- + "-"
- + "".join(random.choices(string.ascii_lowercase + string.digits, k=4))
- + "-"
- + "".join(random.choices(string.ascii_lowercase + string.digits, k=4))
- )
-
- ri = redis_instance()
-
- key = "magic_" + str(email)
-
- # Check if the key already exists in python
- if ri.exists(key):
- data = json.loads(ri.get(key))
-
- current_attempt = data["current_attempt"] + 1
-
- if data["current_attempt"] > 2:
- return Response(
- {"error": "Max attempts exhausted. Please try again later."},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- value = {
- "current_attempt": current_attempt,
- "email": email,
- "token": token,
- }
- expiry = 600
-
- ri.set(key, json.dumps(value), ex=expiry)
-
- else:
- value = {"current_attempt": 0, "email": email, "token": token}
- expiry = 600
-
- ri.set(key, json.dumps(value), ex=expiry)
-
- current_site = settings.WEB_URL
- magic_link.delay(email, key, token, current_site)
-
- return Response({"key": key}, status=status.HTTP_200_OK)
- except ValidationError:
- return Response(
- {"error": "Please provide a valid email address."},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class MagicSignInEndpoint(BaseAPIView):
- permission_classes = [
- AllowAny,
- ]
-
- def post(self, request):
- try:
- user_token = request.data.get("token", "").strip()
- key = request.data.get("key", False).strip().lower()
-
- if not key or user_token == "":
- return Response(
- {"error": "User token and key are required"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- ri = redis_instance()
-
- if ri.exists(key):
- data = json.loads(ri.get(key))
-
- token = data["token"]
- email = data["email"]
-
- if str(token) == str(user_token):
- if User.objects.filter(email=email).exists():
- user = User.objects.get(email=email)
- # Send event to Jitsu for tracking
- if settings.ANALYTICS_BASE_API:
- _ = requests.post(
- settings.ANALYTICS_BASE_API,
- headers={
- "Content-Type": "application/json",
- "X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
- },
- json={
- "event_id": uuid.uuid4().hex,
- "event_data": {
- "medium": "code",
- },
- "user": {"email": email, "id": str(user.id)},
- "device_ctx": {
- "ip": request.META.get("REMOTE_ADDR"),
- "user_agent": request.META.get(
- "HTTP_USER_AGENT"
- ),
- },
- "event_type": "SIGN_IN",
- },
- )
- else:
- user = User.objects.create(
- email=email,
- username=uuid.uuid4().hex,
- password=make_password(uuid.uuid4().hex),
- is_password_autoset=True,
- )
- # Send event to Jitsu for tracking
- if settings.ANALYTICS_BASE_API:
- _ = requests.post(
- settings.ANALYTICS_BASE_API,
- headers={
- "Content-Type": "application/json",
- "X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
- },
- json={
- "event_id": uuid.uuid4().hex,
- "event_data": {
- "medium": "code",
- },
- "user": {"email": email, "id": str(user.id)},
- "device_ctx": {
- "ip": request.META.get("REMOTE_ADDR"),
- "user_agent": request.META.get(
- "HTTP_USER_AGENT"
- ),
- },
- "event_type": "SIGN_UP",
- },
- )
-
- user.last_active = timezone.now()
- user.last_login_time = timezone.now()
- user.last_login_ip = request.META.get("REMOTE_ADDR")
- user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
- user.token_updated_at = timezone.now()
- user.save()
- serialized_user = UserSerializer(user).data
-
- access_token, refresh_token = get_tokens_for_user(user)
- data = {
- "access_token": access_token,
- "refresh_token": refresh_token,
- "user": serialized_user,
- }
-
- return Response(data, status=status.HTTP_200_OK)
-
- else:
- return Response(
- {"error": "Your login code was incorrect. Please try again."},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- else:
- return Response(
- {"error": "The magic code/link has expired please try again"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
diff --git a/apiserver/plane/api/views/base.py b/apiserver/plane/api/views/base.py
index 60b0ec0c62..abde4e8b01 100644
--- a/apiserver/plane/api/views/base.py
+++ b/apiserver/plane/api/views/base.py
@@ -1,23 +1,25 @@
# Python imports
import zoneinfo
+import json
# Django imports
-from django.urls import resolve
from django.conf import settings
+from django.db import IntegrityError
+from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.utils import timezone
-# Third part imports
-from rest_framework import status
-from rest_framework.viewsets import ModelViewSet
-from rest_framework.exceptions import APIException
+# Third party imports
from rest_framework.views import APIView
-from rest_framework.filters import SearchFilter
+from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
+from rest_framework import status
from sentry_sdk import capture_exception
-from django_filters.rest_framework import DjangoFilterBackend
# Module imports
+from plane.api.middleware.api_authentication import APIKeyAuthentication
+from plane.api.rate_limit import ApiKeyRateThrottle
from plane.utils.paginator import BasePaginator
+from plane.bgtasks.webhook_task import send_webhook
class TimezoneMixin:
@@ -25,6 +27,7 @@ class TimezoneMixin:
This enables timezone conversion according
to the user set timezone
"""
+
def initial(self, request, *args, **kwargs):
super().initial(request, *args, **kwargs)
if request.user.is_authenticated:
@@ -33,86 +36,121 @@ class TimezoneMixin:
timezone.deactivate()
+class WebhookMixin:
+ webhook_event = None
+ bulk = False
+ def finalize_response(self, request, response, *args, **kwargs):
+ response = super().finalize_response(request, response, *args, **kwargs)
-class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
-
- model = None
-
- permission_classes = [
- IsAuthenticated,
- ]
-
- filter_backends = (
- DjangoFilterBackend,
- SearchFilter,
- )
-
- filterset_fields = []
-
- search_fields = []
-
- def get_queryset(self):
- try:
- return self.model.objects.all()
- except Exception as e:
- capture_exception(e)
- raise APIException("Please check the view", status.HTTP_400_BAD_REQUEST)
-
- def dispatch(self, request, *args, **kwargs):
- response = super().dispatch(request, *args, **kwargs)
-
- if settings.DEBUG:
- from django.db import connection
-
- print(
- f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}"
+ # Check for the case should webhook be sent
+ if (
+ self.webhook_event
+ and self.request.method in ["POST", "PATCH", "DELETE"]
+ and response.status_code in [200, 201, 204]
+ ):
+ # Push the object to delay
+ send_webhook.delay(
+ event=self.webhook_event,
+ payload=response.data,
+ kw=self.kwargs,
+ action=self.request.method,
+ slug=self.workspace_slug,
+ bulk=self.bulk,
)
+
return response
- @property
- def workspace_slug(self):
- return self.kwargs.get("slug", None)
-
- @property
- def project_id(self):
- project_id = self.kwargs.get("project_id", None)
- if project_id:
- return project_id
-
- if resolve(self.request.path_info).url_name == "project":
- return self.kwargs.get("pk", None)
-
class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
+ authentication_classes = [
+ APIKeyAuthentication,
+ ]
permission_classes = [
IsAuthenticated,
]
- filter_backends = (
- DjangoFilterBackend,
- SearchFilter,
- )
-
- filterset_fields = []
-
- search_fields = []
+ throttle_classes = [
+ ApiKeyRateThrottle,
+ ]
def filter_queryset(self, queryset):
for backend in list(self.filter_backends):
queryset = backend().filter_queryset(self.request, queryset, self)
return queryset
- def dispatch(self, request, *args, **kwargs):
- response = super().dispatch(request, *args, **kwargs)
+ def handle_exception(self, exc):
+ """
+ Handle any exception that occurs, by returning an appropriate response,
+ or re-raising the error.
+ """
+ try:
+ response = super().handle_exception(exc)
+ return response
+ except Exception as e:
+ if isinstance(e, IntegrityError):
+ return Response(
+ {"error": "The payload is not valid"},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
- if settings.DEBUG:
- from django.db import connection
+ if isinstance(e, ValidationError):
+ return Response(
+ {
+ "error": "The provided payload is not valid please try with a valid payload"
+ },
+ status=status.HTTP_400_BAD_REQUEST,
+ )
- print(
- f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}"
+ if isinstance(e, ObjectDoesNotExist):
+ model_name = str(exc).split(" matching query does not exist.")[0]
+ return Response(
+ {"error": f"{model_name} does not exist."},
+ status=status.HTTP_404_NOT_FOUND,
+ )
+
+ if isinstance(e, KeyError):
+ return Response(
+ {"error": f"key {e} does not exist"},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
+ if settings.DEBUG:
+ print(e)
+ capture_exception(e)
+ return Response(
+ {"error": "Something went wrong please try again later"},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
+
+ def dispatch(self, request, *args, **kwargs):
+ try:
+ response = super().dispatch(request, *args, **kwargs)
+ if settings.DEBUG:
+ from django.db import connection
+
+ print(
+ f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}"
+ )
+ return response
+ except Exception as exc:
+ response = self.handle_exception(exc)
+ return exc
+
+ def finalize_response(self, request, response, *args, **kwargs):
+ # Call super to get the default response
+ response = super().finalize_response(request, response, *args, **kwargs)
+
+ # Add custom headers if they exist in the request META
+ ratelimit_remaining = request.META.get("X-RateLimit-Remaining")
+ if ratelimit_remaining is not None:
+ response["X-RateLimit-Remaining"] = ratelimit_remaining
+
+ ratelimit_reset = request.META.get("X-RateLimit-Reset")
+ if ratelimit_reset is not None:
+ response["X-RateLimit-Reset"] = ratelimit_reset
+
return response
@property
@@ -122,3 +160,17 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
@property
def project_id(self):
return self.kwargs.get("project_id", None)
+
+ @property
+ def fields(self):
+ fields = [
+ field for field in self.request.GET.get("fields", "").split(",") if field
+ ]
+ return fields if fields else None
+
+ @property
+ def expand(self):
+ expand = [
+ expand for expand in self.request.GET.get("expand", "").split(",") if expand
+ ]
+ return expand if expand else None
diff --git a/apiserver/plane/api/views/config.py b/apiserver/plane/api/views/config.py
deleted file mode 100644
index ea1b39d9ce..0000000000
--- a/apiserver/plane/api/views/config.py
+++ /dev/null
@@ -1,40 +0,0 @@
-# Python imports
-import os
-
-# Django imports
-from django.conf import settings
-
-# Third party imports
-from rest_framework.permissions import AllowAny
-from rest_framework import status
-from rest_framework.response import Response
-from sentry_sdk import capture_exception
-
-# Module imports
-from .base import BaseAPIView
-
-
-class ConfigurationEndpoint(BaseAPIView):
- permission_classes = [
- AllowAny,
- ]
-
- def get(self, request):
- try:
- data = {}
- data["google"] = os.environ.get("GOOGLE_CLIENT_ID", None)
- data["github"] = os.environ.get("GITHUB_CLIENT_ID", None)
- data["github_app_name"] = os.environ.get("GITHUB_APP_NAME", None)
- data["magic_login"] = (
- bool(settings.EMAIL_HOST_USER) and bool(settings.EMAIL_HOST_PASSWORD)
- ) and os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "0") == "1"
- data["email_password_login"] = (
- os.environ.get("ENABLE_EMAIL_PASSWORD", "0") == "1"
- )
- return Response(data, status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py
index e84b6dd0ad..310332333f 100644
--- a/apiserver/plane/api/views/cycle.py
+++ b/apiserver/plane/api/views/cycle.py
@@ -2,106 +2,47 @@
import json
# Django imports
-from django.db import IntegrityError
-from django.db.models import (
- OuterRef,
- Func,
- F,
- Q,
- Exists,
- OuterRef,
- Count,
- Prefetch,
- Sum,
-)
-from django.core import serializers
+from django.db.models import Q, Count, Sum, Prefetch, F, OuterRef, Func
from django.utils import timezone
-from django.utils.decorators import method_decorator
-from django.views.decorators.gzip import gzip_page
+from django.core import serializers
# Third party imports
from rest_framework.response import Response
from rest_framework import status
-from sentry_sdk import capture_exception
# Module imports
-from . import BaseViewSet, BaseAPIView
+from .base import BaseAPIView, WebhookMixin
+from plane.db.models import Cycle, Issue, CycleIssue, IssueLink, IssueAttachment
+from plane.app.permissions import ProjectEntityPermission
from plane.api.serializers import (
CycleSerializer,
CycleIssueSerializer,
- CycleFavoriteSerializer,
- IssueStateSerializer,
- CycleWriteSerializer,
-)
-from plane.api.permissions import ProjectEntityPermission
-from plane.db.models import (
- User,
- Cycle,
- CycleIssue,
- Issue,
- CycleFavorite,
- IssueLink,
- IssueAttachment,
- Label,
)
from plane.bgtasks.issue_activites_task import issue_activity
-from plane.utils.grouper import group_results
-from plane.utils.issue_filters import issue_filters
-from plane.utils.analytics_plot import burndown_plot
-class CycleViewSet(BaseViewSet):
+class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
+ """
+ This viewset automatically provides `list`, `create`, `retrieve`,
+ `update` and `destroy` actions related to cycle.
+
+ """
+
serializer_class = CycleSerializer
model = Cycle
+ webhook_event = "cycle"
permission_classes = [
ProjectEntityPermission,
]
- def perform_create(self, serializer):
- serializer.save(
- project_id=self.kwargs.get("project_id"), owned_by=self.request.user
- )
-
- def perform_destroy(self, instance):
- cycle_issues = list(
- CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
- "issue", flat=True
- )
- )
- issue_activity.delay(
- type="cycle.activity.deleted",
- requested_data=json.dumps(
- {
- "cycle_id": str(self.kwargs.get("pk")),
- "issues": [str(issue_id) for issue_id in cycle_issues],
- }
- ),
- actor_id=str(self.request.user.id),
- issue_id=str(self.kwargs.get("pk", None)),
- project_id=str(self.kwargs.get("project_id", None)),
- current_instance=None,
- epoch=int(timezone.now().timestamp())
- )
-
- return super().perform_destroy(instance)
-
def get_queryset(self):
- subquery = CycleFavorite.objects.filter(
- user=self.request.user,
- cycle_id=OuterRef("pk"),
- project_id=self.kwargs.get("project_id"),
- workspace__slug=self.kwargs.get("slug"),
- )
- return self.filter_queryset(
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
+ return (
+ Cycle.objects.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(project__project_projectmember__member=self.request.user)
.select_related("project")
.select_related("workspace")
.select_related("owned_by")
- .annotate(is_favorite=Exists(subquery))
.annotate(
total_issues=Count(
"issue_cycle",
@@ -182,409 +123,202 @@ class CycleViewSet(BaseViewSet):
),
)
)
- .prefetch_related(
- Prefetch(
- "issue_cycle__issue__assignees",
- queryset=User.objects.only("avatar", "first_name", "id").distinct(),
- )
- )
- .prefetch_related(
- Prefetch(
- "issue_cycle__issue__labels",
- queryset=Label.objects.only("name", "color", "id").distinct(),
- )
- )
- .order_by("-is_favorite", "name")
+ .order_by(self.kwargs.get("order_by", "-created_at"))
.distinct()
)
- def list(self, request, slug, project_id):
- try:
- queryset = self.get_queryset()
- cycle_view = request.GET.get("cycle_view", "all")
- order_by = request.GET.get("order_by", "sort_order")
-
- queryset = queryset.order_by(order_by)
-
- # All Cycles
- if cycle_view == "all":
- return Response(
- CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
- )
-
- # Current Cycle
- if cycle_view == "current":
- queryset = queryset.filter(
- start_date__lte=timezone.now(),
- end_date__gte=timezone.now(),
- )
-
- data = CycleSerializer(queryset, many=True).data
-
- if len(data):
- assignee_distribution = (
- Issue.objects.filter(
- issue_cycle__cycle_id=data[0]["id"],
- workspace__slug=slug,
- project_id=project_id,
- )
- .annotate(display_name=F("assignees__display_name"))
- .annotate(assignee_id=F("assignees__id"))
- .annotate(avatar=F("assignees__avatar"))
- .values("display_name", "assignee_id", "avatar")
- .annotate(
- total_issues=Count(
- "assignee_id",
- filter=Q(archived_at__isnull=True, is_draft=False),
- ),
- )
- .annotate(
- completed_issues=Count(
- "assignee_id",
- filter=Q(
- completed_at__isnull=False,
- archived_at__isnull=True,
- is_draft=False,
- ),
- )
- )
- .annotate(
- pending_issues=Count(
- "assignee_id",
- filter=Q(
- completed_at__isnull=True,
- archived_at__isnull=True,
- is_draft=False,
- ),
- )
- )
- .order_by("display_name")
- )
-
- label_distribution = (
- Issue.objects.filter(
- issue_cycle__cycle_id=data[0]["id"],
- workspace__slug=slug,
- project_id=project_id,
- )
- .annotate(label_name=F("labels__name"))
- .annotate(color=F("labels__color"))
- .annotate(label_id=F("labels__id"))
- .values("label_name", "color", "label_id")
- .annotate(
- total_issues=Count(
- "label_id",
- filter=Q(archived_at__isnull=True, is_draft=False),
- )
- )
- .annotate(
- completed_issues=Count(
- "label_id",
- filter=Q(
- completed_at__isnull=False,
- archived_at__isnull=True,
- is_draft=False,
- ),
- )
- )
- .annotate(
- pending_issues=Count(
- "label_id",
- filter=Q(
- completed_at__isnull=True,
- archived_at__isnull=True,
- is_draft=False,
- ),
- )
- )
- .order_by("label_name")
- )
- data[0]["distribution"] = {
- "assignees": assignee_distribution,
- "labels": label_distribution,
- "completion_chart": {},
- }
- if data[0]["start_date"] and data[0]["end_date"]:
- data[0]["distribution"]["completion_chart"] = burndown_plot(
- queryset=queryset.first(),
- slug=slug,
- project_id=project_id,
- cycle_id=data[0]["id"],
- )
-
- return Response(data, status=status.HTTP_200_OK)
-
- # Upcoming Cycles
- if cycle_view == "upcoming":
- queryset = queryset.filter(start_date__gt=timezone.now())
- return Response(
- CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
- )
-
- # Completed Cycles
- if cycle_view == "completed":
- queryset = queryset.filter(end_date__lt=timezone.now())
- return Response(
- CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
- )
-
- # Draft Cycles
- if cycle_view == "draft":
- queryset = queryset.filter(
- end_date=None,
- start_date=None,
- )
-
- return Response(
- CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
- )
-
- # Incomplete Cycles
- if cycle_view == "incomplete":
- queryset = queryset.filter(
- Q(end_date__gte=timezone.now().date()) | Q(end_date__isnull=True),
- )
- return Response(
- CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
- )
-
- return Response(
- {"error": "No matching view found"}, status=status.HTTP_400_BAD_REQUEST
- )
-
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def create(self, request, slug, project_id):
- try:
- if (
- request.data.get("start_date", None) is None
- and request.data.get("end_date", None) is None
- ) or (
- request.data.get("start_date", None) is not None
- and request.data.get("end_date", None) is not None
- ):
- serializer = CycleSerializer(data=request.data)
- if serializer.is_valid():
- serializer.save(
- project_id=project_id,
- owned_by=request.user,
- )
- return Response(serializer.data, status=status.HTTP_201_CREATED)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- else:
- return Response(
- {
- "error": "Both start date and end date are either required or are to be null"
- },
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def partial_update(self, request, slug, project_id, pk):
- try:
- cycle = Cycle.objects.get(
- workspace__slug=slug, project_id=project_id, pk=pk
- )
-
- request_data = request.data
-
- if cycle.end_date is not None and cycle.end_date < timezone.now().date():
- if "sort_order" in request_data:
- # Can only change sort order
- request_data = {
- "sort_order": request_data.get("sort_order", cycle.sort_order)
- }
- else:
- return Response(
- {
- "error": "The Cycle has already been completed so it cannot be edited"
- },
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- serializer = CycleWriteSerializer(cycle, data=request.data, partial=True)
- if serializer.is_valid():
- serializer.save()
- return Response(serializer.data, status=status.HTTP_200_OK)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- except Cycle.DoesNotExist:
- return Response(
- {"error": "Cycle does not exist"}, status=status.HTTP_400_BAD_REQUEST
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def retrieve(self, request, slug, project_id, pk):
- try:
+ def get(self, request, slug, project_id, pk=None):
+ if pk:
queryset = self.get_queryset().get(pk=pk)
-
- # Assignee Distribution
- assignee_distribution = (
- Issue.objects.filter(
- issue_cycle__cycle_id=pk,
- workspace__slug=slug,
- project_id=project_id,
- )
- .annotate(first_name=F("assignees__first_name"))
- .annotate(last_name=F("assignees__last_name"))
- .annotate(assignee_id=F("assignees__id"))
- .annotate(avatar=F("assignees__avatar"))
- .annotate(display_name=F("assignees__display_name"))
- .values(
- "first_name", "last_name", "assignee_id", "avatar", "display_name"
- )
- .annotate(
- total_issues=Count(
- "assignee_id",
- filter=Q(archived_at__isnull=True, is_draft=False),
- ),
- )
- .annotate(
- completed_issues=Count(
- "assignee_id",
- filter=Q(
- completed_at__isnull=False,
- archived_at__isnull=True,
- is_draft=False,
- ),
- )
- )
- .annotate(
- pending_issues=Count(
- "assignee_id",
- filter=Q(
- completed_at__isnull=True,
- archived_at__isnull=True,
- is_draft=False,
- ),
- )
- )
- .order_by("first_name", "last_name")
- )
-
- # Label Distribution
- label_distribution = (
- Issue.objects.filter(
- issue_cycle__cycle_id=pk,
- workspace__slug=slug,
- project_id=project_id,
- )
- .annotate(label_name=F("labels__name"))
- .annotate(color=F("labels__color"))
- .annotate(label_id=F("labels__id"))
- .values("label_name", "color", "label_id")
- .annotate(
- total_issues=Count(
- "label_id",
- filter=Q(archived_at__isnull=True, is_draft=False),
- ),
- )
- .annotate(
- completed_issues=Count(
- "label_id",
- filter=Q(
- completed_at__isnull=False,
- archived_at__isnull=True,
- is_draft=False,
- ),
- )
- )
- .annotate(
- pending_issues=Count(
- "label_id",
- filter=Q(
- completed_at__isnull=True,
- archived_at__isnull=True,
- is_draft=False,
- ),
- )
- )
- .order_by("label_name")
- )
-
- data = CycleSerializer(queryset).data
- data["distribution"] = {
- "assignees": assignee_distribution,
- "labels": label_distribution,
- "completion_chart": {},
- }
-
- if queryset.start_date and queryset.end_date:
- data["distribution"]["completion_chart"] = burndown_plot(
- queryset=queryset, slug=slug, project_id=project_id, cycle_id=pk
- )
-
+ data = CycleSerializer(
+ queryset,
+ fields=self.fields,
+ expand=self.expand,
+ ).data
return Response(
data,
status=status.HTTP_200_OK,
)
- except Cycle.DoesNotExist:
- return Response(
- {"error": "Cycle Does not exists"}, status=status.HTTP_400_BAD_REQUEST
+ queryset = self.get_queryset()
+ cycle_view = request.GET.get("cycle_view", "all")
+
+ # Current Cycle
+ if cycle_view == "current":
+ queryset = queryset.filter(
+ start_date__lte=timezone.now(),
+ end_date__gte=timezone.now(),
)
- except Exception as e:
- capture_exception(e)
+ data = CycleSerializer(
+ queryset, many=True, fields=self.fields, expand=self.expand
+ ).data
+ return Response(data, status=status.HTTP_200_OK)
+
+ # Upcoming Cycles
+ if cycle_view == "upcoming":
+ queryset = queryset.filter(start_date__gt=timezone.now())
+ return self.paginate(
+ request=request,
+ queryset=(queryset),
+ on_results=lambda cycles: CycleSerializer(
+ cycles,
+ many=True,
+ fields=self.fields,
+ expand=self.expand,
+ ).data,
+ )
+
+ # Completed Cycles
+ if cycle_view == "completed":
+ queryset = queryset.filter(end_date__lt=timezone.now())
+ return self.paginate(
+ request=request,
+ queryset=(queryset),
+ on_results=lambda cycles: CycleSerializer(
+ cycles,
+ many=True,
+ fields=self.fields,
+ expand=self.expand,
+ ).data,
+ )
+
+ # Draft Cycles
+ if cycle_view == "draft":
+ queryset = queryset.filter(
+ end_date=None,
+ start_date=None,
+ )
+ return self.paginate(
+ request=request,
+ queryset=(queryset),
+ on_results=lambda cycles: CycleSerializer(
+ cycles,
+ many=True,
+ fields=self.fields,
+ expand=self.expand,
+ ).data,
+ )
+
+ # Incomplete Cycles
+ if cycle_view == "incomplete":
+ queryset = queryset.filter(
+ Q(end_date__gte=timezone.now().date()) | Q(end_date__isnull=True),
+ )
+ return self.paginate(
+ request=request,
+ queryset=(queryset),
+ on_results=lambda cycles: CycleSerializer(
+ cycles,
+ many=True,
+ fields=self.fields,
+ expand=self.expand,
+ ).data,
+ )
+ return self.paginate(
+ request=request,
+ queryset=(queryset),
+ on_results=lambda cycles: CycleSerializer(
+ cycles,
+ many=True,
+ fields=self.fields,
+ expand=self.expand,
+ ).data,
+ )
+
+ def post(self, request, slug, project_id):
+ if (
+ request.data.get("start_date", None) is None
+ and request.data.get("end_date", None) is None
+ ) or (
+ request.data.get("start_date", None) is not None
+ and request.data.get("end_date", None) is not None
+ ):
+ serializer = CycleSerializer(data=request.data)
+ if serializer.is_valid():
+ serializer.save(
+ project_id=project_id,
+ owned_by=request.user,
+ )
+ return Response(serializer.data, status=status.HTTP_201_CREATED)
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+ else:
return Response(
- {"error": "Something went wrong please try again later"},
+ {
+ "error": "Both start date and end date are either required or are to be null"
+ },
status=status.HTTP_400_BAD_REQUEST,
)
+ def patch(self, request, slug, project_id, pk):
+ cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
-class CycleIssueViewSet(BaseViewSet):
- serializer_class = CycleIssueSerializer
- model = CycleIssue
+ request_data = request.data
- permission_classes = [
- ProjectEntityPermission,
- ]
+ if cycle.end_date is not None and cycle.end_date < timezone.now().date():
+ if "sort_order" in request_data:
+ # Can only change sort order
+ request_data = {
+ "sort_order": request_data.get("sort_order", cycle.sort_order)
+ }
+ else:
+ return Response(
+ {
+ "error": "The Cycle has already been completed so it cannot be edited"
+ },
+ status=status.HTTP_400_BAD_REQUEST,
+ )
- filterset_fields = [
- "issue__labels__id",
- "issue__assignees__id",
- ]
+ serializer = CycleSerializer(cycle, data=request.data, partial=True)
+ if serializer.is_valid():
+ serializer.save()
+ return Response(serializer.data, status=status.HTTP_200_OK)
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- def perform_create(self, serializer):
- serializer.save(
- project_id=self.kwargs.get("project_id"),
- cycle_id=self.kwargs.get("cycle_id"),
+ def delete(self, request, slug, project_id, pk):
+ cycle_issues = list(
+ CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
+ "issue", flat=True
+ )
)
+ cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
- def perform_destroy(self, instance):
issue_activity.delay(
type="cycle.activity.deleted",
requested_data=json.dumps(
{
- "cycle_id": str(self.kwargs.get("cycle_id")),
- "issues": [str(instance.issue_id)],
+ "cycle_id": str(pk),
+ "cycle_name": str(cycle.name),
+ "issues": [str(issue_id) for issue_id in cycle_issues],
}
),
- actor_id=str(self.request.user.id),
- issue_id=str(self.kwargs.get("pk", None)),
- project_id=str(self.kwargs.get("project_id", None)),
+ actor_id=str(request.user.id),
+ issue_id=None,
+ project_id=str(project_id),
current_instance=None,
- epoch=int(timezone.now().timestamp())
+ epoch=int(timezone.now().timestamp()),
)
- return super().perform_destroy(instance)
+ # Delete the cycle
+ cycle.delete()
+ return Response(status=status.HTTP_204_NO_CONTENT)
+
+
+class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
+ """
+ This viewset automatically provides `list`, `create`,
+ and `destroy` actions related to cycle issues.
+
+ """
+
+ serializer_class = CycleIssueSerializer
+ model = CycleIssue
+ webhook_event = "cycle_issue"
+ bulk = True
+ permission_classes = [
+ ProjectEntityPermission,
+ ]
def get_queryset(self):
- return self.filter_queryset(
- super()
- .get_queryset()
- .annotate(
+ return (
+ CycleIssue.objects.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue_id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
@@ -599,340 +333,221 @@ class CycleIssueViewSet(BaseViewSet):
.select_related("cycle")
.select_related("issue", "issue__state", "issue__project")
.prefetch_related("issue__assignees", "issue__labels")
+ .order_by(self.kwargs.get("order_by", "-created_at"))
.distinct()
)
- @method_decorator(gzip_page)
- def list(self, request, slug, project_id, cycle_id):
- try:
- order_by = request.GET.get("order_by", "created_at")
- group_by = request.GET.get("group_by", False)
- sub_group_by = request.GET.get("sub_group_by", False)
- filters = issue_filters(request.query_params, "GET")
- issues = (
- Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
- .annotate(
- sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- .annotate(bridge_id=F("issue_cycle__id"))
- .filter(project_id=project_id)
- .filter(workspace__slug=slug)
- .select_related("project")
- .select_related("workspace")
- .select_related("state")
- .select_related("parent")
- .prefetch_related("assignees")
- .prefetch_related("labels")
- .order_by(order_by)
- .filter(**filters)
- .annotate(
- link_count=IssueLink.objects.filter(issue=OuterRef("id"))
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- .annotate(
- attachment_count=IssueAttachment.objects.filter(
- issue=OuterRef("id")
- )
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
+ def get(self, request, slug, project_id, cycle_id):
+ order_by = request.GET.get("order_by", "created_at")
+ issues = (
+ Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
+ .annotate(
+ sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
+ .order_by()
+ .annotate(count=Func(F("id"), function="Count"))
+ .values("count")
)
-
- issues_data = IssueStateSerializer(issues, many=True).data
-
- if sub_group_by and sub_group_by == group_by:
- return Response(
- {"error": "Group by and sub group by cannot be same"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- if group_by:
- return Response(
- group_results(issues_data, group_by, sub_group_by),
- status=status.HTTP_200_OK,
- )
-
- return Response(
- issues_data,
- status=status.HTTP_200_OK,
+ .annotate(bridge_id=F("issue_cycle__id"))
+ .filter(project_id=project_id)
+ .filter(workspace__slug=slug)
+ .select_related("project")
+ .select_related("workspace")
+ .select_related("state")
+ .select_related("parent")
+ .prefetch_related("assignees")
+ .prefetch_related("labels")
+ .order_by(order_by)
+ .annotate(
+ link_count=IssueLink.objects.filter(issue=OuterRef("id"))
+ .order_by()
+ .annotate(count=Func(F("id"), function="Count"))
+ .values("count")
)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
+ .annotate(
+ attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
+ .order_by()
+ .annotate(count=Func(F("id"), function="Count"))
+ .values("count")
)
-
- def create(self, request, slug, project_id, cycle_id):
- try:
- issues = request.data.get("issues", [])
-
- if not len(issues):
- return Response(
- {"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
- )
-
- cycle = Cycle.objects.get(
- workspace__slug=slug, project_id=project_id, pk=cycle_id
- )
-
- if cycle.end_date is not None and cycle.end_date < timezone.now().date():
- return Response(
- {
- "error": "The Cycle has already been completed so no new issues can be added"
- },
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- # Get all CycleIssues already created
- cycle_issues = list(CycleIssue.objects.filter(issue_id__in=issues))
- update_cycle_issue_activity = []
- record_to_create = []
- records_to_update = []
-
- for issue in issues:
- cycle_issue = [
- cycle_issue
- for cycle_issue in cycle_issues
- if str(cycle_issue.issue_id) in issues
- ]
- # Update only when cycle changes
- if len(cycle_issue):
- if cycle_issue[0].cycle_id != cycle_id:
- update_cycle_issue_activity.append(
- {
- "old_cycle_id": str(cycle_issue[0].cycle_id),
- "new_cycle_id": str(cycle_id),
- "issue_id": str(cycle_issue[0].issue_id),
- }
- )
- cycle_issue[0].cycle_id = cycle_id
- records_to_update.append(cycle_issue[0])
- else:
- record_to_create.append(
- CycleIssue(
- project_id=project_id,
- workspace=cycle.workspace,
- created_by=request.user,
- updated_by=request.user,
- cycle=cycle,
- issue_id=issue,
- )
- )
-
- CycleIssue.objects.bulk_create(
- record_to_create,
- batch_size=10,
- ignore_conflicts=True,
- )
- CycleIssue.objects.bulk_update(
- records_to_update,
- ["cycle"],
- batch_size=10,
- )
-
- # Capture Issue Activity
- issue_activity.delay(
- type="cycle.activity.created",
- requested_data=json.dumps({"cycles_list": issues}),
- actor_id=str(self.request.user.id),
- issue_id=str(self.kwargs.get("pk", None)),
- project_id=str(self.kwargs.get("project_id", None)),
- current_instance=json.dumps(
- {
- "updated_cycle_issues": update_cycle_issue_activity,
- "created_cycle_issues": serializers.serialize(
- "json", record_to_create
- ),
- }
- ),
- epoch=int(timezone.now().timestamp())
- )
-
- # Return all Cycle Issues
- return Response(
- CycleIssueSerializer(self.get_queryset(), many=True).data,
- status=status.HTTP_200_OK,
- )
-
- except Cycle.DoesNotExist:
- return Response(
- {"error": "Cycle not found"}, status=status.HTTP_404_NOT_FOUND
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class CycleDateCheckEndpoint(BaseAPIView):
- permission_classes = [
- ProjectEntityPermission,
- ]
-
- def post(self, request, slug, project_id):
- try:
- start_date = request.data.get("start_date", False)
- end_date = request.data.get("end_date", False)
- cycle_id = request.data.get("cycle_id")
- if not start_date or not end_date:
- return Response(
- {"error": "Start date and end date both are required"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- cycles = Cycle.objects.filter(
- Q(workspace__slug=slug)
- & Q(project_id=project_id)
- & (
- Q(start_date__lte=start_date, end_date__gte=start_date)
- | Q(start_date__lte=end_date, end_date__gte=end_date)
- | Q(start_date__gte=start_date, end_date__lte=end_date)
- )
- ).exclude(pk=cycle_id)
-
- if cycles.exists():
- return Response(
- {
- "error": "You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates",
- "status": False,
- }
- )
- else:
- return Response({"status": True}, status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class CycleFavoriteViewSet(BaseViewSet):
- serializer_class = CycleFavoriteSerializer
- model = CycleFavorite
-
- def get_queryset(self):
- return self.filter_queryset(
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
- .filter(user=self.request.user)
- .select_related("cycle", "cycle__owned_by")
)
- def create(self, request, slug, project_id):
- try:
- serializer = CycleFavoriteSerializer(data=request.data)
- if serializer.is_valid():
- serializer.save(user=request.user, project_id=project_id)
- return Response(serializer.data, status=status.HTTP_201_CREATED)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- except IntegrityError as e:
- if "already exists" in str(e):
- return Response(
- {"error": "The cycle is already added to favorites"},
- status=status.HTTP_410_GONE,
- )
+ return self.paginate(
+ request=request,
+ queryset=(issues),
+ on_results=lambda issues: CycleSerializer(
+ issues,
+ many=True,
+ fields=self.fields,
+ expand=self.expand,
+ ).data,
+ )
+
+ def post(self, request, slug, project_id, cycle_id):
+ issues = request.data.get("issues", [])
+
+ if not issues:
+ return Response(
+ {"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
+ )
+
+ cycle = Cycle.objects.get(
+ workspace__slug=slug, project_id=project_id, pk=cycle_id
+ )
+
+ if cycle.end_date is not None and cycle.end_date < timezone.now().date():
+ return Response(
+ {
+ "error": "The Cycle has already been completed so no new issues can be added"
+ },
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
+ issues = Issue.objects.filter(
+ pk__in=issues, workspace__slug=slug, project_id=project_id
+ ).values_list("id", flat=True)
+
+ # Get all CycleIssues already created
+ cycle_issues = list(CycleIssue.objects.filter(issue_id__in=issues))
+ update_cycle_issue_activity = []
+ record_to_create = []
+ records_to_update = []
+
+ for issue in issues:
+ cycle_issue = [
+ cycle_issue
+ for cycle_issue in cycle_issues
+ if str(cycle_issue.issue_id) in issues
+ ]
+ # Update only when cycle changes
+ if len(cycle_issue):
+ if cycle_issue[0].cycle_id != cycle_id:
+ update_cycle_issue_activity.append(
+ {
+ "old_cycle_id": str(cycle_issue[0].cycle_id),
+ "new_cycle_id": str(cycle_id),
+ "issue_id": str(cycle_issue[0].issue_id),
+ }
+ )
+ cycle_issue[0].cycle_id = cycle_id
+ records_to_update.append(cycle_issue[0])
else:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
+ record_to_create.append(
+ CycleIssue(
+ project_id=project_id,
+ workspace=cycle.workspace,
+ created_by=request.user,
+ updated_by=request.user,
+ cycle=cycle,
+ issue_id=issue,
+ )
)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- def destroy(self, request, slug, project_id, cycle_id):
- try:
- cycle_favorite = CycleFavorite.objects.get(
- project=project_id,
- user=request.user,
- workspace__slug=slug,
- cycle_id=cycle_id,
- )
- cycle_favorite.delete()
- return Response(status=status.HTTP_204_NO_CONTENT)
- except CycleFavorite.DoesNotExist:
- return Response(
- {"error": "Cycle is not in favorites"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
+ CycleIssue.objects.bulk_create(
+ record_to_create,
+ batch_size=10,
+ ignore_conflicts=True,
+ )
+ CycleIssue.objects.bulk_update(
+ records_to_update,
+ ["cycle"],
+ batch_size=10,
+ )
+
+ # Capture Issue Activity
+ issue_activity.delay(
+ type="cycle.activity.created",
+ requested_data=json.dumps({"cycles_list": str(issues)}),
+ actor_id=str(self.request.user.id),
+ issue_id=None,
+ project_id=str(self.kwargs.get("project_id", None)),
+ current_instance=json.dumps(
+ {
+ "updated_cycle_issues": update_cycle_issue_activity,
+ "created_cycle_issues": serializers.serialize(
+ "json", record_to_create
+ ),
+ }
+ ),
+ epoch=int(timezone.now().timestamp()),
+ )
+
+ # Return all Cycle Issues
+ return Response(
+ CycleIssueSerializer(self.get_queryset(), many=True).data,
+ status=status.HTTP_200_OK,
+ )
+
+ def delete(self, request, slug, project_id, cycle_id, issue_id):
+ cycle_issue = CycleIssue.objects.get(
+ issue_id=issue_id, workspace__slug=slug, project_id=project_id, cycle_id=cycle_id
+ )
+ issue_id = cycle_issue.issue_id
+ cycle_issue.delete()
+ issue_activity.delay(
+ type="cycle.activity.deleted",
+ requested_data=json.dumps(
+ {
+ "cycle_id": str(self.kwargs.get("cycle_id")),
+ "issues": [str(issue_id)],
+ }
+ ),
+ actor_id=str(self.request.user.id),
+ issue_id=str(issue_id),
+ project_id=str(self.kwargs.get("project_id", None)),
+ current_instance=None,
+ epoch=int(timezone.now().timestamp()),
+ )
+ return Response(status=status.HTTP_204_NO_CONTENT)
-class TransferCycleIssueEndpoint(BaseAPIView):
+class TransferCycleIssueAPIEndpoint(BaseAPIView):
+ """
+ This viewset provides `create` actions for transfering the issues into a particular cycle.
+
+ """
+
permission_classes = [
ProjectEntityPermission,
]
def post(self, request, slug, project_id, cycle_id):
- try:
- new_cycle_id = request.data.get("new_cycle_id", False)
+ new_cycle_id = request.data.get("new_cycle_id", False)
- if not new_cycle_id:
- return Response(
- {"error": "New Cycle Id is required"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- new_cycle = Cycle.objects.get(
- workspace__slug=slug, project_id=project_id, pk=new_cycle_id
- )
-
- if (
- new_cycle.end_date is not None
- and new_cycle.end_date < timezone.now().date()
- ):
- return Response(
- {
- "error": "The cycle where the issues are transferred is already completed"
- },
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- cycle_issues = CycleIssue.objects.filter(
- cycle_id=cycle_id,
- project_id=project_id,
- workspace__slug=slug,
- issue__state__group__in=["backlog", "unstarted", "started"],
- )
-
- updated_cycles = []
- for cycle_issue in cycle_issues:
- cycle_issue.cycle_id = new_cycle_id
- updated_cycles.append(cycle_issue)
-
- cycle_issues = CycleIssue.objects.bulk_update(
- updated_cycles, ["cycle_id"], batch_size=100
- )
-
- return Response({"message": "Success"}, status=status.HTTP_200_OK)
- except Cycle.DoesNotExist:
+ if not new_cycle_id:
return Response(
- {"error": "New Cycle Does not exist"},
+ {"error": "New Cycle Id is required"},
status=status.HTTP_400_BAD_REQUEST,
)
- except Exception as e:
- capture_exception(e)
+
+ new_cycle = Cycle.objects.get(
+ workspace__slug=slug, project_id=project_id, pk=new_cycle_id
+ )
+
+ if (
+ new_cycle.end_date is not None
+ and new_cycle.end_date < timezone.now().date()
+ ):
return Response(
- {"error": "Something went wrong please try again later"},
+ {
+ "error": "The cycle where the issues are transferred is already completed"
+ },
status=status.HTTP_400_BAD_REQUEST,
)
+
+ cycle_issues = CycleIssue.objects.filter(
+ cycle_id=cycle_id,
+ project_id=project_id,
+ workspace__slug=slug,
+ issue__state__group__in=["backlog", "unstarted", "started"],
+ )
+
+ updated_cycles = []
+ for cycle_issue in cycle_issues:
+ cycle_issue.cycle_id = new_cycle_id
+ updated_cycles.append(cycle_issue)
+
+ cycle_issues = CycleIssue.objects.bulk_update(
+ updated_cycles, ["cycle_id"], batch_size=100
+ )
+
+ return Response({"message": "Success"}, status=status.HTTP_200_OK)
\ No newline at end of file
diff --git a/apiserver/plane/api/views/estimate.py b/apiserver/plane/api/views/estimate.py
deleted file mode 100644
index 68de54d7ae..0000000000
--- a/apiserver/plane/api/views/estimate.py
+++ /dev/null
@@ -1,253 +0,0 @@
-# Django imports
-from django.db import IntegrityError
-
-# Third party imports
-from rest_framework.response import Response
-from rest_framework import status
-from sentry_sdk import capture_exception
-
-# Module imports
-from .base import BaseViewSet, BaseAPIView
-from plane.api.permissions import ProjectEntityPermission
-from plane.db.models import Project, Estimate, EstimatePoint
-from plane.api.serializers import (
- EstimateSerializer,
- EstimatePointSerializer,
- EstimateReadSerializer,
-)
-
-
-class ProjectEstimatePointEndpoint(BaseAPIView):
- permission_classes = [
- ProjectEntityPermission,
- ]
-
- def get(self, request, slug, project_id):
- try:
- project = Project.objects.get(workspace__slug=slug, pk=project_id)
- if project.estimate_id is not None:
- estimate_points = EstimatePoint.objects.filter(
- estimate_id=project.estimate_id,
- project_id=project_id,
- workspace__slug=slug,
- )
- serializer = EstimatePointSerializer(estimate_points, many=True)
- return Response(serializer.data, status=status.HTTP_200_OK)
- return Response([], status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class BulkEstimatePointEndpoint(BaseViewSet):
- permission_classes = [
- ProjectEntityPermission,
- ]
- model = Estimate
- serializer_class = EstimateSerializer
-
- def list(self, request, slug, project_id):
- try:
- estimates = Estimate.objects.filter(
- workspace__slug=slug, project_id=project_id
- ).prefetch_related("points").select_related("workspace", "project")
- serializer = EstimateReadSerializer(estimates, many=True)
- return Response(serializer.data, status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def create(self, request, slug, project_id):
- try:
- if not request.data.get("estimate", False):
- return Response(
- {"error": "Estimate is required"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- estimate_points = request.data.get("estimate_points", [])
-
- if not len(estimate_points) or len(estimate_points) > 8:
- return Response(
- {"error": "Estimate points are required"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- estimate_serializer = EstimateSerializer(data=request.data.get("estimate"))
- if not estimate_serializer.is_valid():
- return Response(
- estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST
- )
- try:
- estimate = estimate_serializer.save(project_id=project_id)
- except IntegrityError:
- return Response(
- {"errror": "Estimate with the name already exists"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- estimate_points = EstimatePoint.objects.bulk_create(
- [
- EstimatePoint(
- estimate=estimate,
- key=estimate_point.get("key", 0),
- value=estimate_point.get("value", ""),
- description=estimate_point.get("description", ""),
- project_id=project_id,
- workspace_id=estimate.workspace_id,
- created_by=request.user,
- updated_by=request.user,
- )
- for estimate_point in estimate_points
- ],
- batch_size=10,
- ignore_conflicts=True,
- )
-
- estimate_point_serializer = EstimatePointSerializer(
- estimate_points, many=True
- )
-
- return Response(
- {
- "estimate": estimate_serializer.data,
- "estimate_points": estimate_point_serializer.data,
- },
- status=status.HTTP_200_OK,
- )
- except Estimate.DoesNotExist:
- return Response(
- {"error": "Estimate does not exist"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def retrieve(self, request, slug, project_id, estimate_id):
- try:
- estimate = Estimate.objects.get(
- pk=estimate_id, workspace__slug=slug, project_id=project_id
- )
- serializer = EstimateReadSerializer(estimate)
- return Response(
- serializer.data,
- status=status.HTTP_200_OK,
- )
- except Estimate.DoesNotExist:
- return Response(
- {"error": "Estimate does not exist"}, status=status.HTTP_400_BAD_REQUEST
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def partial_update(self, request, slug, project_id, estimate_id):
- try:
- if not request.data.get("estimate", False):
- return Response(
- {"error": "Estimate is required"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- if not len(request.data.get("estimate_points", [])):
- return Response(
- {"error": "Estimate points are required"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- estimate = Estimate.objects.get(pk=estimate_id)
-
- estimate_serializer = EstimateSerializer(
- estimate, data=request.data.get("estimate"), partial=True
- )
- if not estimate_serializer.is_valid():
- return Response(
- estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST
- )
- try:
- estimate = estimate_serializer.save()
- except IntegrityError:
- return Response(
- {"errror": "Estimate with the name already exists"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- estimate_points_data = request.data.get("estimate_points", [])
-
- estimate_points = EstimatePoint.objects.filter(
- pk__in=[
- estimate_point.get("id") for estimate_point in estimate_points_data
- ],
- workspace__slug=slug,
- project_id=project_id,
- estimate_id=estimate_id,
- )
-
- updated_estimate_points = []
- for estimate_point in estimate_points:
- # Find the data for that estimate point
- estimate_point_data = [
- point
- for point in estimate_points_data
- if point.get("id") == str(estimate_point.id)
- ]
- if len(estimate_point_data):
- estimate_point.value = estimate_point_data[0].get(
- "value", estimate_point.value
- )
- updated_estimate_points.append(estimate_point)
-
- try:
- EstimatePoint.objects.bulk_update(
- updated_estimate_points, ["value"], batch_size=10,
- )
- except IntegrityError as e:
- return Response(
- {"error": "Values need to be unique for each key"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- estimate_point_serializer = EstimatePointSerializer(estimate_points, many=True)
- return Response(
- {
- "estimate": estimate_serializer.data,
- "estimate_points": estimate_point_serializer.data,
- },
- status=status.HTTP_200_OK,
- )
- except Estimate.DoesNotExist:
- return Response(
- {"error": "Estimate does not exist"}, status=status.HTTP_400_BAD_REQUEST
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def destroy(self, request, slug, project_id, estimate_id):
- try:
- estimate = Estimate.objects.get(
- pk=estimate_id, workspace__slug=slug, project_id=project_id
- )
- estimate.delete()
- return Response(status=status.HTTP_204_NO_CONTENT)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
diff --git a/apiserver/plane/api/views/exporter.py b/apiserver/plane/api/views/exporter.py
deleted file mode 100644
index 7e14aa82f5..0000000000
--- a/apiserver/plane/api/views/exporter.py
+++ /dev/null
@@ -1,100 +0,0 @@
-# Third Party imports
-from rest_framework.response import Response
-from rest_framework import status
-from sentry_sdk import capture_exception
-
-# Module imports
-from . import BaseAPIView
-from plane.api.permissions import WorkSpaceAdminPermission
-from plane.bgtasks.export_task import issue_export_task
-from plane.db.models import Project, ExporterHistory, Workspace
-
-from plane.api.serializers import ExporterHistorySerializer
-
-
-class ExportIssuesEndpoint(BaseAPIView):
- permission_classes = [
- WorkSpaceAdminPermission,
- ]
- model = ExporterHistory
- serializer_class = ExporterHistorySerializer
-
- def post(self, request, slug):
- try:
- # Get the workspace
- workspace = Workspace.objects.get(slug=slug)
-
- provider = request.data.get("provider", False)
- multiple = request.data.get("multiple", False)
- project_ids = request.data.get("project", [])
-
- if provider in ["csv", "xlsx", "json"]:
- if not project_ids:
- project_ids = Project.objects.filter(
- workspace__slug=slug
- ).values_list("id", flat=True)
- project_ids = [str(project_id) for project_id in project_ids]
-
- exporter = ExporterHistory.objects.create(
- workspace=workspace,
- project=project_ids,
- initiated_by=request.user,
- provider=provider,
- )
-
- issue_export_task.delay(
- provider=exporter.provider,
- workspace_id=workspace.id,
- project_ids=project_ids,
- token_id=exporter.token,
- multiple=multiple,
- slug=slug,
- )
- return Response(
- {
- "message": f"Once the export is ready you will be able to download it"
- },
- status=status.HTTP_200_OK,
- )
- else:
- return Response(
- {"error": f"Provider '{provider}' not found."},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Workspace.DoesNotExist:
- return Response(
- {"error": "Workspace does not exists"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def get(self, request, slug):
- try:
- exporter_history = ExporterHistory.objects.filter(
- workspace__slug=slug
- ).select_related("workspace","initiated_by")
-
- if request.GET.get("per_page", False) and request.GET.get("cursor", False):
- return self.paginate(
- request=request,
- queryset=exporter_history,
- on_results=lambda exporter_history: ExporterHistorySerializer(
- exporter_history, many=True
- ).data,
- )
- else:
- return Response(
- {"error": "per_page and cursor are required"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
diff --git a/apiserver/plane/api/views/external.py b/apiserver/plane/api/views/external.py
deleted file mode 100644
index 00a0270e49..0000000000
--- a/apiserver/plane/api/views/external.py
+++ /dev/null
@@ -1,118 +0,0 @@
-# Python imports
-import requests
-
-# Third party imports
-import openai
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework.permissions import AllowAny
-from sentry_sdk import capture_exception
-
-# Django imports
-from django.conf import settings
-
-# Module imports
-from .base import BaseAPIView
-from plane.api.permissions import ProjectEntityPermission
-from plane.db.models import Workspace, Project
-from plane.api.serializers import ProjectLiteSerializer, WorkspaceLiteSerializer
-from plane.utils.integrations.github import get_release_notes
-
-
-class GPTIntegrationEndpoint(BaseAPIView):
- permission_classes = [
- ProjectEntityPermission,
- ]
-
- def post(self, request, slug, project_id):
- try:
- if not settings.OPENAI_API_KEY or not settings.GPT_ENGINE:
- return Response(
- {"error": "OpenAI API key and engine is required"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- prompt = request.data.get("prompt", False)
- task = request.data.get("task", False)
-
- if not task:
- return Response(
- {"error": "Task is required"}, status=status.HTTP_400_BAD_REQUEST
- )
-
- final_text = task + "\n" + prompt
-
- openai.api_key = settings.OPENAI_API_KEY
- response = openai.ChatCompletion.create(
- model=settings.GPT_ENGINE,
- messages=[{"role": "user", "content": final_text}],
- temperature=0.7,
- max_tokens=1024,
- )
-
- workspace = Workspace.objects.get(slug=slug)
- project = Project.objects.get(pk=project_id)
-
- text = response.choices[0].message.content.strip()
- text_html = text.replace("\n", "
")
- return Response(
- {
- "response": text,
- "response_html": text_html,
- "project_detail": ProjectLiteSerializer(project).data,
- "workspace_detail": WorkspaceLiteSerializer(workspace).data,
- },
- status=status.HTTP_200_OK,
- )
- except (Workspace.DoesNotExist, Project.DoesNotExist) as e:
- return Response(
- {"error": "Workspace or Project Does not exist"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class ReleaseNotesEndpoint(BaseAPIView):
- def get(self, request):
- try:
- release_notes = get_release_notes()
- return Response(release_notes, status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class UnsplashEndpoint(BaseAPIView):
-
- def get(self, request):
- try:
- query = request.GET.get("query", False)
- page = request.GET.get("page", 1)
- per_page = request.GET.get("per_page", 20)
-
- url = (
- f"https://api.unsplash.com/search/photos/?client_id={settings.UNSPLASH_ACCESS_KEY}&query={query}&page=${page}&per_page={per_page}"
- if query
- else f"https://api.unsplash.com/photos/?client_id={settings.UNSPLASH_ACCESS_KEY}&page={page}&per_page={per_page}"
- )
-
- headers = {
- "Content-Type": "application/json",
- }
-
- resp = requests.get(url=url, headers=headers)
- return Response(resp.json(), status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
diff --git a/apiserver/plane/api/views/importer.py b/apiserver/plane/api/views/importer.py
deleted file mode 100644
index 18d9a1d693..0000000000
--- a/apiserver/plane/api/views/importer.py
+++ /dev/null
@@ -1,602 +0,0 @@
-# Python imports
-import uuid
-
-# Third party imports
-from rest_framework import status
-from rest_framework.response import Response
-from sentry_sdk import capture_exception
-
-# Django imports
-from django.db.models import Max, Q
-
-# Module imports
-from plane.api.views import BaseAPIView
-from plane.db.models import (
- WorkspaceIntegration,
- Importer,
- APIToken,
- Project,
- State,
- IssueSequence,
- Issue,
- IssueActivity,
- IssueComment,
- IssueLink,
- IssueLabel,
- Workspace,
- IssueAssignee,
- Module,
- ModuleLink,
- ModuleIssue,
- Label,
-)
-from plane.api.serializers import (
- ImporterSerializer,
- IssueFlatSerializer,
- ModuleSerializer,
-)
-from plane.utils.integrations.github import get_github_repo_details
-from plane.utils.importers.jira import jira_project_issue_summary
-from plane.bgtasks.importer_task import service_importer
-from plane.utils.html_processor import strip_tags
-
-
-class ServiceIssueImportSummaryEndpoint(BaseAPIView):
-
- def get(self, request, slug, service):
- try:
- if service == "github":
- owner = request.GET.get("owner", False)
- repo = request.GET.get("repo", False)
-
- if not owner or not repo:
- return Response(
- {"error": "Owner and repo are required"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- workspace_integration = WorkspaceIntegration.objects.get(
- integration__provider="github", workspace__slug=slug
- )
-
- access_tokens_url = workspace_integration.metadata.get(
- "access_tokens_url", False
- )
-
- if not access_tokens_url:
- return Response(
- {
- "error": "There was an error during the installation of the GitHub app. To resolve this issue, we recommend reinstalling the GitHub app."
- },
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- issue_count, labels, collaborators = get_github_repo_details(
- access_tokens_url, owner, repo
- )
- return Response(
- {
- "issue_count": issue_count,
- "labels": labels,
- "collaborators": collaborators,
- },
- status=status.HTTP_200_OK,
- )
-
- if service == "jira":
- # Check for all the keys
- params = {
- "project_key": "Project key is required",
- "api_token": "API token is required",
- "email": "Email is required",
- "cloud_hostname": "Cloud hostname is required",
- }
-
- for key, error_message in params.items():
- if not request.GET.get(key, False):
- return Response(
- {"error": error_message}, status=status.HTTP_400_BAD_REQUEST
- )
-
- project_key = request.GET.get("project_key", "")
- api_token = request.GET.get("api_token", "")
- email = request.GET.get("email", "")
- cloud_hostname = request.GET.get("cloud_hostname", "")
-
- response = jira_project_issue_summary(
- email, api_token, project_key, cloud_hostname
- )
- if "error" in response:
- return Response(response, status=status.HTTP_400_BAD_REQUEST)
- else:
- return Response(
- response,
- status=status.HTTP_200_OK,
- )
- return Response(
- {"error": "Service not supported yet"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except WorkspaceIntegration.DoesNotExist:
- return Response(
- {"error": "Requested integration was not installed in the workspace"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class ImportServiceEndpoint(BaseAPIView):
- def post(self, request, slug, service):
- try:
- project_id = request.data.get("project_id", False)
-
- if not project_id:
- return Response(
- {"error": "Project ID is required"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- workspace = Workspace.objects.get(slug=slug)
-
- if service == "github":
- data = request.data.get("data", False)
- metadata = request.data.get("metadata", False)
- config = request.data.get("config", False)
- if not data or not metadata or not config:
- return Response(
- {"error": "Data, config and metadata are required"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- api_token = APIToken.objects.filter(
- user=request.user, workspace=workspace
- ).first()
- if api_token is None:
- api_token = APIToken.objects.create(
- user=request.user,
- label="Importer",
- workspace=workspace,
- )
-
- importer = Importer.objects.create(
- service=service,
- project_id=project_id,
- status="queued",
- initiated_by=request.user,
- data=data,
- metadata=metadata,
- token=api_token,
- config=config,
- created_by=request.user,
- updated_by=request.user,
- )
-
- service_importer.delay(service, importer.id)
- serializer = ImporterSerializer(importer)
- return Response(serializer.data, status=status.HTTP_201_CREATED)
-
- if service == "jira":
- data = request.data.get("data", False)
- metadata = request.data.get("metadata", False)
- config = request.data.get("config", False)
- if not data or not metadata:
- return Response(
- {"error": "Data, config and metadata are required"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- api_token = APIToken.objects.filter(
- user=request.user, workspace=workspace
- ).first()
- if api_token is None:
- api_token = APIToken.objects.create(
- user=request.user,
- label="Importer",
- workspace=workspace,
- )
-
- importer = Importer.objects.create(
- service=service,
- project_id=project_id,
- status="queued",
- initiated_by=request.user,
- data=data,
- metadata=metadata,
- token=api_token,
- config=config,
- created_by=request.user,
- updated_by=request.user,
- )
-
- service_importer.delay(service, importer.id)
- serializer = ImporterSerializer(importer)
- return Response(serializer.data, status=status.HTTP_201_CREATED)
-
- return Response(
- {"error": "Servivce not supported yet"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except (
- Workspace.DoesNotExist,
- WorkspaceIntegration.DoesNotExist,
- Project.DoesNotExist,
- ) as e:
- return Response(
- {"error": "Workspace Integration or Project does not exist"},
- status=status.HTTP_404_NOT_FOUND,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def get(self, request, slug):
- try:
- imports = (
- Importer.objects.filter(workspace__slug=slug)
- .order_by("-created_at")
- .select_related("initiated_by", "project", "workspace")
- )
- serializer = ImporterSerializer(imports, many=True)
- return Response(serializer.data)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def delete(self, request, slug, service, pk):
- try:
- importer = Importer.objects.get(
- pk=pk, service=service, workspace__slug=slug
- )
-
- if importer.imported_data is not None:
- # Delete all imported Issues
- imported_issues = importer.imported_data.get("issues", [])
- Issue.issue_objects.filter(id__in=imported_issues).delete()
-
- # Delete all imported Labels
- imported_labels = importer.imported_data.get("labels", [])
- Label.objects.filter(id__in=imported_labels).delete()
-
- if importer.service == "jira":
- imported_modules = importer.imported_data.get("modules", [])
- Module.objects.filter(id__in=imported_modules).delete()
- importer.delete()
- return Response(status=status.HTTP_204_NO_CONTENT)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def patch(self, request, slug, service, pk):
- try:
- importer = Importer.objects.get(
- pk=pk, service=service, workspace__slug=slug
- )
- serializer = ImporterSerializer(importer, data=request.data, partial=True)
- if serializer.is_valid():
- serializer.save()
- return Response(serializer.data, status=status.HTTP_200_OK)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- except Importer.DoesNotExist:
- return Response(
- {"error": "Importer Does not exists"}, status=status.HTTP_404_NOT_FOUND
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class UpdateServiceImportStatusEndpoint(BaseAPIView):
- def post(self, request, slug, project_id, service, importer_id):
- try:
- importer = Importer.objects.get(
- pk=importer_id,
- workspace__slug=slug,
- project_id=project_id,
- service=service,
- )
- importer.status = request.data.get("status", "processing")
- importer.save()
- return Response(status.HTTP_200_OK)
- except Importer.DoesNotExist:
- return Response(
- {"error": "Importer does not exist"}, status=status.HTTP_404_NOT_FOUND
- )
-
-
-class BulkImportIssuesEndpoint(BaseAPIView):
- def post(self, request, slug, project_id, service):
- try:
- # Get the project
- project = Project.objects.get(pk=project_id, workspace__slug=slug)
-
- # Get the default state
- default_state = State.objects.filter(
- ~Q(name="Triage"), project_id=project_id, default=True
- ).first()
- # if there is no default state assign any random state
- if default_state is None:
- default_state = State.objects.filter(
- ~Q(name="Triage"), project_id=project_id
- ).first()
-
- # Get the maximum sequence_id
- last_id = IssueSequence.objects.filter(project_id=project_id).aggregate(
- largest=Max("sequence")
- )["largest"]
-
- last_id = 1 if last_id is None else last_id + 1
-
- # Get the maximum sort order
- largest_sort_order = Issue.objects.filter(
- project_id=project_id, state=default_state
- ).aggregate(largest=Max("sort_order"))["largest"]
-
- largest_sort_order = (
- 65535 if largest_sort_order is None else largest_sort_order + 10000
- )
-
- # Get the issues_data
- issues_data = request.data.get("issues_data", [])
-
- if not len(issues_data):
- return Response(
- {"error": "Issue data is required"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- # Issues
- bulk_issues = []
- for issue_data in issues_data:
- bulk_issues.append(
- Issue(
- project_id=project_id,
- workspace_id=project.workspace_id,
- state_id=issue_data.get("state")
- if issue_data.get("state", False)
- else default_state.id,
- name=issue_data.get("name", "Issue Created through Bulk"),
- description_html=issue_data.get("description_html", ""),
- description_stripped=(
- None
- if (
- issue_data.get("description_html") == ""
- or issue_data.get("description_html") is None
- )
- else strip_tags(issue_data.get("description_html"))
- ),
- sequence_id=last_id,
- sort_order=largest_sort_order,
- start_date=issue_data.get("start_date", None),
- target_date=issue_data.get("target_date", None),
- priority=issue_data.get("priority", "none"),
- created_by=request.user,
- )
- )
-
- largest_sort_order = largest_sort_order + 10000
- last_id = last_id + 1
-
- issues = Issue.objects.bulk_create(
- bulk_issues,
- batch_size=100,
- ignore_conflicts=True,
- )
-
- # Sequences
- _ = IssueSequence.objects.bulk_create(
- [
- IssueSequence(
- issue=issue,
- sequence=issue.sequence_id,
- project_id=project_id,
- workspace_id=project.workspace_id,
- )
- for issue in issues
- ],
- batch_size=100,
- )
-
- # Attach Labels
- bulk_issue_labels = []
- for issue, issue_data in zip(issues, issues_data):
- labels_list = issue_data.get("labels_list", [])
- bulk_issue_labels = bulk_issue_labels + [
- IssueLabel(
- issue=issue,
- label_id=label_id,
- project_id=project_id,
- workspace_id=project.workspace_id,
- created_by=request.user,
- )
- for label_id in labels_list
- ]
-
- _ = IssueLabel.objects.bulk_create(
- bulk_issue_labels, batch_size=100, ignore_conflicts=True
- )
-
- # Attach Assignees
- bulk_issue_assignees = []
- for issue, issue_data in zip(issues, issues_data):
- assignees_list = issue_data.get("assignees_list", [])
- bulk_issue_assignees = bulk_issue_assignees + [
- IssueAssignee(
- issue=issue,
- assignee_id=assignee_id,
- project_id=project_id,
- workspace_id=project.workspace_id,
- created_by=request.user,
- )
- for assignee_id in assignees_list
- ]
-
- _ = IssueAssignee.objects.bulk_create(
- bulk_issue_assignees, batch_size=100, ignore_conflicts=True
- )
-
- # Track the issue activities
- IssueActivity.objects.bulk_create(
- [
- IssueActivity(
- issue=issue,
- actor=request.user,
- project_id=project_id,
- workspace_id=project.workspace_id,
- comment=f"imported the issue from {service}",
- verb="created",
- created_by=request.user,
- )
- for issue in issues
- ],
- batch_size=100,
- )
-
- # Create Comments
- bulk_issue_comments = []
- for issue, issue_data in zip(issues, issues_data):
- comments_list = issue_data.get("comments_list", [])
- bulk_issue_comments = bulk_issue_comments + [
- IssueComment(
- issue=issue,
- comment_html=comment.get("comment_html", ""),
- actor=request.user,
- project_id=project_id,
- workspace_id=project.workspace_id,
- created_by=request.user,
- )
- for comment in comments_list
- ]
-
- _ = IssueComment.objects.bulk_create(bulk_issue_comments, batch_size=100)
-
- # Attach Links
- _ = IssueLink.objects.bulk_create(
- [
- IssueLink(
- issue=issue,
- url=issue_data.get("link", {}).get("url", "https://github.com"),
- title=issue_data.get("link", {}).get("title", "Original Issue"),
- project_id=project_id,
- workspace_id=project.workspace_id,
- created_by=request.user,
- )
- for issue, issue_data in zip(issues, issues_data)
- ]
- )
-
- return Response(
- {"issues": IssueFlatSerializer(issues, many=True).data},
- status=status.HTTP_201_CREATED,
- )
- except Project.DoesNotExist:
- return Response(
- {"error": "Project Does not exist"}, status=status.HTTP_404_NOT_FOUND
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class BulkImportModulesEndpoint(BaseAPIView):
- def post(self, request, slug, project_id, service):
- try:
- modules_data = request.data.get("modules_data", [])
- project = Project.objects.get(pk=project_id, workspace__slug=slug)
-
- modules = Module.objects.bulk_create(
- [
- Module(
- name=module.get("name", uuid.uuid4().hex),
- description=module.get("description", ""),
- start_date=module.get("start_date", None),
- target_date=module.get("target_date", None),
- project_id=project_id,
- workspace_id=project.workspace_id,
- created_by=request.user,
- )
- for module in modules_data
- ],
- batch_size=100,
- ignore_conflicts=True,
- )
-
- modules = Module.objects.filter(id__in=[module.id for module in modules])
-
- if len(modules) == len(modules_data):
- _ = ModuleLink.objects.bulk_create(
- [
- ModuleLink(
- module=module,
- url=module_data.get("link", {}).get(
- "url", "https://plane.so"
- ),
- title=module_data.get("link", {}).get(
- "title", "Original Issue"
- ),
- project_id=project_id,
- workspace_id=project.workspace_id,
- created_by=request.user,
- )
- for module, module_data in zip(modules, modules_data)
- ],
- batch_size=100,
- ignore_conflicts=True,
- )
-
- bulk_module_issues = []
- for module, module_data in zip(modules, modules_data):
- module_issues_list = module_data.get("module_issues_list", [])
- bulk_module_issues = bulk_module_issues + [
- ModuleIssue(
- issue_id=issue,
- module=module,
- project_id=project_id,
- workspace_id=project.workspace_id,
- created_by=request.user,
- )
- for issue in module_issues_list
- ]
-
- _ = ModuleIssue.objects.bulk_create(
- bulk_module_issues, batch_size=100, ignore_conflicts=True
- )
-
- serializer = ModuleSerializer(modules, many=True)
- return Response(
- {"modules": serializer.data}, status=status.HTTP_201_CREATED
- )
-
- else:
- return Response(
- {"message": "Modules created but issues could not be imported"},
- status=status.HTTP_200_OK,
- )
- except Project.DoesNotExist:
- return Response(
- {"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py
index 4bfc32f019..94ddc4f109 100644
--- a/apiserver/plane/api/views/inbox.py
+++ b/apiserver/plane/api/views/inbox.py
@@ -1,90 +1,30 @@
# Python imports
import json
-# Django import
+# Django improts
from django.utils import timezone
-from django.db.models import Q, Count, OuterRef, Func, F, Prefetch
+from django.db.models import Q
from django.core.serializers.json import DjangoJSONEncoder
# Third party imports
from rest_framework import status
from rest_framework.response import Response
-from sentry_sdk import capture_exception
# Module imports
-from .base import BaseViewSet
-from plane.api.permissions import ProjectBasePermission, ProjectLitePermission
-from plane.db.models import (
- Inbox,
- InboxIssue,
- Issue,
- State,
- IssueLink,
- IssueAttachment,
- ProjectMember,
- ProjectDeployBoard,
-)
-from plane.api.serializers import (
- IssueSerializer,
- InboxSerializer,
- InboxIssueSerializer,
- IssueCreateSerializer,
- IssueStateInboxSerializer,
-)
-from plane.utils.issue_filters import issue_filters
+from .base import BaseAPIView
+from plane.app.permissions import ProjectLitePermission
+from plane.api.serializers import InboxIssueSerializer, IssueSerializer
+from plane.db.models import InboxIssue, Issue, State, ProjectMember, Project, Inbox
from plane.bgtasks.issue_activites_task import issue_activity
-class InboxViewSet(BaseViewSet):
- permission_classes = [
- ProjectBasePermission,
- ]
+class InboxIssueAPIEndpoint(BaseAPIView):
+ """
+ This viewset automatically provides `list`, `create`, `retrieve`,
+ `update` and `destroy` actions related to inbox issues.
- serializer_class = InboxSerializer
- model = Inbox
+ """
- def get_queryset(self):
- return (
- super()
- .get_queryset()
- .filter(
- workspace__slug=self.kwargs.get("slug"),
- project_id=self.kwargs.get("project_id"),
- )
- .annotate(
- pending_issue_count=Count(
- "issue_inbox",
- filter=Q(issue_inbox__status=-2),
- )
- )
- .select_related("workspace", "project")
- )
-
- def perform_create(self, serializer):
- serializer.save(project_id=self.kwargs.get("project_id"))
-
- def destroy(self, request, slug, project_id, pk):
- try:
- inbox = Inbox.objects.get(
- workspace__slug=slug, project_id=project_id, pk=pk
- )
- # Handle default inbox delete
- if inbox.is_default:
- return Response(
- {"error": "You cannot delete the default inbox"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- inbox.delete()
- return Response(status=status.HTTP_204_NO_CONTENT)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wronf please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class InboxIssueViewSet(BaseViewSet):
permission_classes = [
ProjectLitePermission,
]
@@ -97,483 +37,195 @@ class InboxIssueViewSet(BaseViewSet):
]
def get_queryset(self):
- return self.filter_queryset(
- super()
- .get_queryset()
- .filter(
+ inbox = Inbox.objects.filter(
+ workspace__slug=self.kwargs.get("slug"),
+ project_id=self.kwargs.get("project_id"),
+ ).first()
+
+ project = Project.objects.get(
+ workspace__slug=self.kwargs.get("slug"), pk=self.kwargs.get("project_id")
+ )
+
+ if inbox is None and not project.inbox_view:
+ return InboxIssue.objects.none()
+
+ return (
+ InboxIssue.objects.filter(
Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"),
- inbox_id=self.kwargs.get("inbox_id"),
+ inbox_id=inbox.id,
)
.select_related("issue", "workspace", "project")
+ .order_by(self.kwargs.get("order_by", "-created_at"))
)
- def list(self, request, slug, project_id, inbox_id):
- try:
- filters = issue_filters(request.query_params, "GET")
- issues = (
- Issue.objects.filter(
- issue_inbox__inbox_id=inbox_id,
- workspace__slug=slug,
- project_id=project_id,
- )
- .filter(**filters)
- .annotate(bridge_id=F("issue_inbox__id"))
- .select_related("workspace", "project", "state", "parent")
- .prefetch_related("assignees", "labels")
- .order_by("issue_inbox__snoozed_till", "issue_inbox__status")
- .annotate(
- sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- .annotate(
- link_count=IssueLink.objects.filter(issue=OuterRef("id"))
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- .annotate(
- attachment_count=IssueAttachment.objects.filter(
- issue=OuterRef("id")
- )
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- .prefetch_related(
- Prefetch(
- "issue_inbox",
- queryset=InboxIssue.objects.only(
- "status", "duplicate_to", "snoozed_till", "source"
- ),
- )
- )
- )
- issues_data = IssueStateInboxSerializer(issues, many=True).data
+ def get(self, request, slug, project_id, issue_id=None):
+ if issue_id:
+ inbox_issue_queryset = self.get_queryset().get(issue_id=issue_id)
+ inbox_issue_data = InboxIssueSerializer(
+ inbox_issue_queryset,
+ fields=self.fields,
+ expand=self.expand,
+ ).data
return Response(
- issues_data,
+ inbox_issue_data,
status=status.HTTP_200_OK,
)
+ issue_queryset = self.get_queryset()
+ return self.paginate(
+ request=request,
+ queryset=(issue_queryset),
+ on_results=lambda inbox_issues: InboxIssueSerializer(
+ inbox_issues,
+ many=True,
+ fields=self.fields,
+ expand=self.expand,
+ ).data,
+ )
- except Exception as e:
- capture_exception(e)
+ def post(self, request, slug, project_id):
+ if not request.data.get("issue", {}).get("name", False):
return Response(
- {"error": "Something went wrong please try again later"},
+ {"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
+ )
+
+ inbox = Inbox.objects.filter(
+ workspace__slug=slug, project_id=project_id
+ ).first()
+
+ project = Project.objects.get(
+ workspace__slug=slug,
+ pk=project_id,
+ )
+
+ # Inbox view
+ if inbox is None and not project.inbox_view:
+ return Response(
+ {
+ "error": "Inbox is not enabled for this project enable it through the project settings"
+ },
status=status.HTTP_400_BAD_REQUEST,
)
- def create(self, request, slug, project_id, inbox_id):
- try:
- if not request.data.get("issue", {}).get("name", False):
- return Response(
- {"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
- )
-
- # Check for valid priority
- if not request.data.get("issue", {}).get("priority", "none") in [
- "low",
- "medium",
- "high",
- "urgent",
- "none",
- ]:
- return Response(
- {"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
- )
-
- # Create or get state
- state, _ = State.objects.get_or_create(
- name="Triage",
- group="backlog",
- description="Default state for managing all Inbox Issues",
- project_id=project_id,
- color="#ff7700",
- )
-
- # create an issue
- issue = Issue.objects.create(
- name=request.data.get("issue", {}).get("name"),
- description=request.data.get("issue", {}).get("description", {}),
- description_html=request.data.get("issue", {}).get(
- "description_html", ""
- ),
- priority=request.data.get("issue", {}).get("priority", "low"),
- project_id=project_id,
- state=state,
- )
-
- # Create an Issue Activity
- issue_activity.delay(
- type="issue.activity.created",
- requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
- actor_id=str(request.user.id),
- issue_id=str(issue.id),
- project_id=str(project_id),
- current_instance=None,
- epoch=int(timezone.now().timestamp())
- )
- # create an inbox issue
- InboxIssue.objects.create(
- inbox_id=inbox_id,
- project_id=project_id,
- issue=issue,
- source=request.data.get("source", "in-app"),
- )
-
- serializer = IssueStateInboxSerializer(issue)
- return Response(serializer.data, status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
+ # Check for valid priority
+ if not request.data.get("issue", {}).get("priority", "none") in [
+ "low",
+ "medium",
+ "high",
+ "urgent",
+ "none",
+ ]:
return Response(
- {"error": "Something went wrong please try again later"},
+ {"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # Create or get state
+ state, _ = State.objects.get_or_create(
+ name="Triage",
+ group="backlog",
+ description="Default state for managing all Inbox Issues",
+ project_id=project_id,
+ color="#ff7700",
+ )
+
+ # create an issue
+ issue = Issue.objects.create(
+ name=request.data.get("issue", {}).get("name"),
+ description=request.data.get("issue", {}).get("description", {}),
+ description_html=request.data.get("issue", {}).get(
+ "description_html", ""
+ ),
+ priority=request.data.get("issue", {}).get("priority", "low"),
+ project_id=project_id,
+ state=state,
+ )
+
+ # Create an Issue Activity
+ issue_activity.delay(
+ type="issue.activity.created",
+ requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
+ actor_id=str(request.user.id),
+ issue_id=str(issue.id),
+ project_id=str(project_id),
+ current_instance=None,
+ epoch=int(timezone.now().timestamp()),
+ )
+
+ # create an inbox issue
+ inbox_issue = InboxIssue.objects.create(
+ inbox_id=inbox.id,
+ project_id=project_id,
+ issue=issue,
+ source=request.data.get("source", "in-app"),
+ )
+
+ serializer = InboxIssueSerializer(inbox_issue)
+ return Response(serializer.data, status=status.HTTP_200_OK)
+
+ def patch(self, request, slug, project_id, issue_id):
+ inbox = Inbox.objects.filter(
+ workspace__slug=slug, project_id=project_id
+ ).first()
+
+ project = Project.objects.get(
+ workspace__slug=slug,
+ pk=project_id,
+ )
+
+ # Inbox view
+ if inbox is None and not project.inbox_view:
+ return Response(
+ {
+ "error": "Inbox is not enabled for this project enable it through the project settings"
+ },
status=status.HTTP_400_BAD_REQUEST,
)
- def partial_update(self, request, slug, project_id, inbox_id, pk):
- try:
- inbox_issue = InboxIssue.objects.get(
- pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
- )
- # Get the project member
- project_member = ProjectMember.objects.get(workspace__slug=slug, project_id=project_id, member=request.user)
- # Only project members admins and created_by users can access this endpoint
- if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(request.user.id):
- return Response({"error": "You cannot edit inbox issues"}, status=status.HTTP_400_BAD_REQUEST)
+ # Get the inbox issue
+ inbox_issue = InboxIssue.objects.get(
+ issue_id=issue_id,
+ workspace__slug=slug,
+ project_id=project_id,
+ inbox_id=inbox.id,
+ )
- # Get issue data
- issue_data = request.data.pop("issue", False)
+ # Get the project member
+ project_member = ProjectMember.objects.get(
+ workspace__slug=slug,
+ project_id=project_id,
+ member=request.user,
+ is_active=True,
+ )
- if bool(issue_data):
- issue = Issue.objects.get(
- pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
- )
- # Only allow guests and viewers to edit name and description
- if project_member.role <= 10:
- # viewers and guests since only viewers and guests
- issue_data = {
- "name": issue_data.get("name", issue.name),
- "description_html": issue_data.get("description_html", issue.description_html),
- "description": issue_data.get("description", issue.description)
- }
-
- issue_serializer = IssueCreateSerializer(
- issue, data=issue_data, partial=True
- )
-
- if issue_serializer.is_valid():
- current_instance = issue
- # Log all the updates
- requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder)
- if issue is not None:
- issue_activity.delay(
- type="issue.activity.updated",
- requested_data=requested_data,
- actor_id=str(request.user.id),
- issue_id=str(issue.id),
- project_id=str(project_id),
- current_instance=json.dumps(
- IssueSerializer(current_instance).data,
- cls=DjangoJSONEncoder,
- ),
- epoch=int(timezone.now().timestamp())
- )
- issue_serializer.save()
- else:
- return Response(
- issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST
- )
-
- # Only project admins and members can edit inbox issue attributes
- if project_member.role > 10:
- serializer = InboxIssueSerializer(
- inbox_issue, data=request.data, partial=True
- )
-
- if serializer.is_valid():
- serializer.save()
- # Update the issue state if the issue is rejected or marked as duplicate
- if serializer.data["status"] in [-1, 2]:
- issue = Issue.objects.get(
- pk=inbox_issue.issue_id,
- workspace__slug=slug,
- project_id=project_id,
- )
- state = State.objects.filter(
- group="cancelled", workspace__slug=slug, project_id=project_id
- ).first()
- if state is not None:
- issue.state = state
- issue.save()
-
- # Update the issue state if it is accepted
- if serializer.data["status"] in [1]:
- issue = Issue.objects.get(
- pk=inbox_issue.issue_id,
- workspace__slug=slug,
- project_id=project_id,
- )
-
- # Update the issue state only if it is in triage state
- if issue.state.name == "Triage":
- # Move to default state
- state = State.objects.filter(
- workspace__slug=slug, project_id=project_id, default=True
- ).first()
- if state is not None:
- issue.state = state
- issue.save()
-
- return Response(serializer.data, status=status.HTTP_200_OK)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- else:
- return Response(InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK)
- except InboxIssue.DoesNotExist:
+ # Only project members admins and created_by users can access this endpoint
+ if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(
+ request.user.id
+ ):
return Response(
- {"error": "Inbox Issue does not exist"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
+ {"error": "You cannot edit inbox issues"},
status=status.HTTP_400_BAD_REQUEST,
)
- def retrieve(self, request, slug, project_id, inbox_id, pk):
- try:
- inbox_issue = InboxIssue.objects.get(
- pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
- )
+ # Get issue data
+ issue_data = request.data.pop("issue", False)
+
+ if bool(issue_data):
issue = Issue.objects.get(
- pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
- )
- serializer = IssueStateInboxSerializer(issue)
- return Response(serializer.data, status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
+ pk=issue_id, workspace__slug=slug, project_id=project_id
)
+ # Only allow guests and viewers to edit name and description
+ if project_member.role <= 10:
+ # viewers and guests since only viewers and guests
+ issue_data = {
+ "name": issue_data.get("name", issue.name),
+ "description_html": issue_data.get(
+ "description_html", issue.description_html
+ ),
+ "description": issue_data.get("description", issue.description),
+ }
- def destroy(self, request, slug, project_id, inbox_id, pk):
- try:
- inbox_issue = InboxIssue.objects.get(
- pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
- )
- # Get the project member
- project_member = ProjectMember.objects.get(workspace__slug=slug, project_id=project_id, member=request.user)
-
- if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(request.user.id):
- return Response({"error": "You cannot delete inbox issue"}, status=status.HTTP_400_BAD_REQUEST)
-
- # Check the issue status
- if inbox_issue.status in [-2, -1, 0, 2]:
- # Delete the issue also
- Issue.objects.filter(workspace__slug=slug, project_id=project_id, pk=inbox_issue.issue_id).delete()
-
- inbox_issue.delete()
- return Response(status=status.HTTP_204_NO_CONTENT)
- except InboxIssue.DoesNotExist:
- return Response({"error": "Inbox Issue does not exists"}, status=status.HTTP_400_BAD_REQUEST)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class InboxIssuePublicViewSet(BaseViewSet):
- serializer_class = InboxIssueSerializer
- model = InboxIssue
-
- filterset_fields = [
- "status",
- ]
-
- def get_queryset(self):
- project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=self.kwargs.get("slug"), project_id=self.kwargs.get("project_id"))
- if project_deploy_board is not None:
- return self.filter_queryset(
- super()
- .get_queryset()
- .filter(
- Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
- project_id=self.kwargs.get("project_id"),
- workspace__slug=self.kwargs.get("slug"),
- inbox_id=self.kwargs.get("inbox_id"),
- )
- .select_related("issue", "workspace", "project")
- )
- else:
- return InboxIssue.objects.none()
-
- def list(self, request, slug, project_id, inbox_id):
- try:
- project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
- if project_deploy_board.inbox is None:
- return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
-
- filters = issue_filters(request.query_params, "GET")
- issues = (
- Issue.objects.filter(
- issue_inbox__inbox_id=inbox_id,
- workspace__slug=slug,
- project_id=project_id,
- )
- .filter(**filters)
- .annotate(bridge_id=F("issue_inbox__id"))
- .select_related("workspace", "project", "state", "parent")
- .prefetch_related("assignees", "labels")
- .order_by("issue_inbox__snoozed_till", "issue_inbox__status")
- .annotate(
- sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- .annotate(
- link_count=IssueLink.objects.filter(issue=OuterRef("id"))
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- .annotate(
- attachment_count=IssueAttachment.objects.filter(
- issue=OuterRef("id")
- )
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- .prefetch_related(
- Prefetch(
- "issue_inbox",
- queryset=InboxIssue.objects.only(
- "status", "duplicate_to", "snoozed_till", "source"
- ),
- )
- )
- )
- issues_data = IssueStateInboxSerializer(issues, many=True).data
- return Response(
- issues_data,
- status=status.HTTP_200_OK,
- )
- except ProjectDeployBoard.DoesNotExist:
- return Response({"error": "Project Deploy Board does not exist"}, status=status.HTTP_400_BAD_REQUEST)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def create(self, request, slug, project_id, inbox_id):
- try:
- project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
- if project_deploy_board.inbox is None:
- return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
-
- if not request.data.get("issue", {}).get("name", False):
- return Response(
- {"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
- )
-
- # Check for valid priority
- if not request.data.get("issue", {}).get("priority", "none") in [
- "low",
- "medium",
- "high",
- "urgent",
- "none",
- ]:
- return Response(
- {"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
- )
-
- # Create or get state
- state, _ = State.objects.get_or_create(
- name="Triage",
- group="backlog",
- description="Default state for managing all Inbox Issues",
- project_id=project_id,
- color="#ff7700",
- )
-
- # create an issue
- issue = Issue.objects.create(
- name=request.data.get("issue", {}).get("name"),
- description=request.data.get("issue", {}).get("description", {}),
- description_html=request.data.get("issue", {}).get(
- "description_html", ""
- ),
- priority=request.data.get("issue", {}).get("priority", "low"),
- project_id=project_id,
- state=state,
- )
-
- # Create an Issue Activity
- issue_activity.delay(
- type="issue.activity.created",
- requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
- actor_id=str(request.user.id),
- issue_id=str(issue.id),
- project_id=str(project_id),
- current_instance=None,
- epoch=int(timezone.now().timestamp())
- )
- # create an inbox issue
- InboxIssue.objects.create(
- inbox_id=inbox_id,
- project_id=project_id,
- issue=issue,
- source=request.data.get("source", "in-app"),
- )
-
- serializer = IssueStateInboxSerializer(issue)
- return Response(serializer.data, status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def partial_update(self, request, slug, project_id, inbox_id, pk):
- try:
- project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
- if project_deploy_board.inbox is None:
- return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
-
- inbox_issue = InboxIssue.objects.get(
- pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
- )
- # Get the project member
- if str(inbox_issue.created_by_id) != str(request.user.id):
- return Response({"error": "You cannot edit inbox issues"}, status=status.HTTP_400_BAD_REQUEST)
-
- # Get issue data
- issue_data = request.data.pop("issue", False)
-
-
- issue = Issue.objects.get(
- pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
- )
- # viewers and guests since only viewers and guests
- issue_data = {
- "name": issue_data.get("name", issue.name),
- "description_html": issue_data.get("description_html", issue.description_html),
- "description": issue_data.get("description", issue.description)
- }
-
- issue_serializer = IssueCreateSerializer(
- issue, data=issue_data, partial=True
- )
+ issue_serializer = IssueSerializer(issue, data=issue_data, partial=True)
if issue_serializer.is_valid():
current_instance = issue
@@ -584,71 +236,117 @@ class InboxIssuePublicViewSet(BaseViewSet):
type="issue.activity.updated",
requested_data=requested_data,
actor_id=str(request.user.id),
- issue_id=str(issue.id),
+ issue_id=str(issue_id),
project_id=str(project_id),
current_instance=json.dumps(
IssueSerializer(current_instance).data,
cls=DjangoJSONEncoder,
),
- epoch=int(timezone.now().timestamp())
+ epoch=int(timezone.now().timestamp()),
)
issue_serializer.save()
- return Response(issue_serializer.data, status=status.HTTP_200_OK)
- return Response(issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- except InboxIssue.DoesNotExist:
- return Response(
- {"error": "Inbox Issue does not exist"},
- status=status.HTTP_400_BAD_REQUEST,
+ else:
+ return Response(
+ issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # Only project admins and members can edit inbox issue attributes
+ if project_member.role > 10:
+ serializer = InboxIssueSerializer(
+ inbox_issue, data=request.data, partial=True
)
- except Exception as e:
- capture_exception(e)
+
+ if serializer.is_valid():
+ serializer.save()
+ # Update the issue state if the issue is rejected or marked as duplicate
+ if serializer.data["status"] in [-1, 2]:
+ issue = Issue.objects.get(
+ pk=issue_id,
+ workspace__slug=slug,
+ project_id=project_id,
+ )
+ state = State.objects.filter(
+ group="cancelled", workspace__slug=slug, project_id=project_id
+ ).first()
+ if state is not None:
+ issue.state = state
+ issue.save()
+
+ # Update the issue state if it is accepted
+ if serializer.data["status"] in [1]:
+ issue = Issue.objects.get(
+ pk=issue_id,
+ workspace__slug=slug,
+ project_id=project_id,
+ )
+
+ # Update the issue state only if it is in triage state
+ if issue.state.name == "Triage":
+ # Move to default state
+ state = State.objects.filter(
+ workspace__slug=slug, project_id=project_id, default=True
+ ).first()
+ if state is not None:
+ issue.state = state
+ issue.save()
+
+ return Response(serializer.data, status=status.HTTP_200_OK)
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+ else:
return Response(
- {"error": "Something went wrong please try again later"},
+ InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK
+ )
+
+ def delete(self, request, slug, project_id, issue_id):
+ inbox = Inbox.objects.filter(
+ workspace__slug=slug, project_id=project_id
+ ).first()
+
+ project = Project.objects.get(
+ workspace__slug=slug,
+ pk=project_id,
+ )
+
+ # Inbox view
+ if inbox is None and not project.inbox_view:
+ return Response(
+ {
+ "error": "Inbox is not enabled for this project enable it through the project settings"
+ },
status=status.HTTP_400_BAD_REQUEST,
)
- def retrieve(self, request, slug, project_id, inbox_id, pk):
- try:
- project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
- if project_deploy_board.inbox is None:
- return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
-
- inbox_issue = InboxIssue.objects.get(
- pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
- )
- issue = Issue.objects.get(
- pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
- )
- serializer = IssueStateInboxSerializer(issue)
- return Response(serializer.data, status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
+ # Get the inbox issue
+ inbox_issue = InboxIssue.objects.get(
+ issue_id=issue_id,
+ workspace__slug=slug,
+ project_id=project_id,
+ inbox_id=inbox.id,
+ )
+
+ # Get the project member
+ project_member = ProjectMember.objects.get(
+ workspace__slug=slug,
+ project_id=project_id,
+ member=request.user,
+ is_active=True,
+ )
+
+ # Check the inbox issue created
+ if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(
+ request.user.id
+ ):
return Response(
- {"error": "Something went wrong please try again later"},
+ {"error": "You cannot delete inbox issue"},
status=status.HTTP_400_BAD_REQUEST,
)
- def destroy(self, request, slug, project_id, inbox_id, pk):
- try:
- project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
- if project_deploy_board.inbox is None:
- return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
-
- inbox_issue = InboxIssue.objects.get(
- pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
- )
-
- if str(inbox_issue.created_by_id) != str(request.user.id):
- return Response({"error": "You cannot delete inbox issue"}, status=status.HTTP_400_BAD_REQUEST)
-
- inbox_issue.delete()
- return Response(status=status.HTTP_204_NO_CONTENT)
- except InboxIssue.DoesNotExist:
- return Response({"error": "Inbox Issue does not exists"}, status=status.HTTP_400_BAD_REQUEST)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
+ # Check the issue status
+ if inbox_issue.status in [-2, -1, 0, 2]:
+ # Delete the issue also
+ Issue.objects.filter(
+ workspace__slug=slug, project_id=project_id, pk=issue_id
+ ).delete()
+ inbox_issue.delete()
+ return Response(status=status.HTTP_204_NO_CONTENT)
diff --git a/apiserver/plane/api/views/integration/base.py b/apiserver/plane/api/views/integration/base.py
deleted file mode 100644
index 5213baf637..0000000000
--- a/apiserver/plane/api/views/integration/base.py
+++ /dev/null
@@ -1,229 +0,0 @@
-# Python improts
-import uuid
-
-# Django imports
-from django.db import IntegrityError
-from django.contrib.auth.hashers import make_password
-
-# Third party imports
-from rest_framework.response import Response
-from rest_framework import status
-from sentry_sdk import capture_exception
-
-# Module imports
-from plane.api.views import BaseViewSet
-from plane.db.models import (
- Integration,
- WorkspaceIntegration,
- Workspace,
- User,
- WorkspaceMember,
- APIToken,
-)
-from plane.api.serializers import IntegrationSerializer, WorkspaceIntegrationSerializer
-from plane.utils.integrations.github import (
- get_github_metadata,
- delete_github_installation,
-)
-from plane.api.permissions import WorkSpaceAdminPermission
-
-
-class IntegrationViewSet(BaseViewSet):
- serializer_class = IntegrationSerializer
- model = Integration
-
- def create(self, request):
- try:
- serializer = IntegrationSerializer(data=request.data)
- if serializer.is_valid():
- serializer.save()
- return Response(serializer.data, status=status.HTTP_201_CREATED)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def partial_update(self, request, pk):
- try:
- integration = Integration.objects.get(pk=pk)
- if integration.verified:
- return Response(
- {"error": "Verified integrations cannot be updated"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- serializer = IntegrationSerializer(
- integration, data=request.data, partial=True
- )
-
- if serializer.is_valid():
- serializer.save()
- return Response(serializer.data, status=status.HTTP_200_OK)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-
- except Integration.DoesNotExist:
- return Response(
- {"error": "Integration Does not exist"},
- status=status.HTTP_404_NOT_FOUND,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def destroy(self, request, pk):
- try:
- integration = Integration.objects.get(pk=pk)
- if integration.verified:
- return Response(
- {"error": "Verified integrations cannot be updated"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- integration.delete()
- return Response(status=status.HTTP_204_NO_CONTENT)
- except Integration.DoesNotExist:
- return Response(
- {"error": "Integration Does not exist"},
- status=status.HTTP_404_NOT_FOUND,
- )
-
-
-class WorkspaceIntegrationViewSet(BaseViewSet):
- serializer_class = WorkspaceIntegrationSerializer
- model = WorkspaceIntegration
-
- permission_classes = [
- WorkSpaceAdminPermission,
- ]
-
- def get_queryset(self):
- return (
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
- .select_related("integration")
- )
-
- def create(self, request, slug, provider):
- try:
- workspace = Workspace.objects.get(slug=slug)
- integration = Integration.objects.get(provider=provider)
- config = {}
- if provider == "github":
- installation_id = request.data.get("installation_id", None)
- if not installation_id:
- return Response(
- {"error": "Installation ID is required"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- metadata = get_github_metadata(installation_id)
- config = {"installation_id": installation_id}
-
- if provider == "slack":
- metadata = request.data.get("metadata", {})
- access_token = metadata.get("access_token", False)
- team_id = metadata.get("team", {}).get("id", False)
- if not metadata or not access_token or not team_id:
- return Response(
- {"error": "Access token and team id is required"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- config = {"team_id": team_id, "access_token": access_token}
-
- # Create a bot user
- bot_user = User.objects.create(
- email=f"{uuid.uuid4().hex}@plane.so",
- username=uuid.uuid4().hex,
- password=make_password(uuid.uuid4().hex),
- is_password_autoset=True,
- is_bot=True,
- first_name=integration.title,
- avatar=integration.avatar_url
- if integration.avatar_url is not None
- else "",
- )
-
- # Create an API Token for the bot user
- api_token = APIToken.objects.create(
- user=bot_user,
- user_type=1, # bot user
- workspace=workspace,
- )
-
- workspace_integration = WorkspaceIntegration.objects.create(
- workspace=workspace,
- integration=integration,
- actor=bot_user,
- api_token=api_token,
- metadata=metadata,
- config=config,
- )
-
- # Add bot user as a member of workspace
- _ = WorkspaceMember.objects.create(
- workspace=workspace_integration.workspace,
- member=bot_user,
- role=20,
- )
- return Response(
- WorkspaceIntegrationSerializer(workspace_integration).data,
- status=status.HTTP_201_CREATED,
- )
- except IntegrityError as e:
- if "already exists" in str(e):
- return Response(
- {"error": "Integration is already active in the workspace"},
- status=status.HTTP_410_GONE,
- )
- else:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except (Workspace.DoesNotExist, Integration.DoesNotExist) as e:
- capture_exception(e)
- return Response(
- {"error": "Workspace or Integration not found"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def destroy(self, request, slug, pk):
- try:
- workspace_integration = WorkspaceIntegration.objects.get(
- pk=pk, workspace__slug=slug
- )
-
- if workspace_integration.integration.provider == "github":
- installation_id = workspace_integration.config.get(
- "installation_id", False
- )
- if installation_id:
- delete_github_installation(installation_id=installation_id)
-
- workspace_integration.delete()
- return Response(status=status.HTTP_204_NO_CONTENT)
-
- except WorkspaceIntegration.DoesNotExist:
- return Response(
- {"error": "Workspace Integration Does not exists"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
diff --git a/apiserver/plane/api/views/integration/github.py b/apiserver/plane/api/views/integration/github.py
deleted file mode 100644
index 4cf07c7059..0000000000
--- a/apiserver/plane/api/views/integration/github.py
+++ /dev/null
@@ -1,231 +0,0 @@
-# Third party imports
-from rest_framework import status
-from rest_framework.response import Response
-from sentry_sdk import capture_exception
-
-# Module imports
-from plane.api.views import BaseViewSet, BaseAPIView
-from plane.db.models import (
- GithubIssueSync,
- GithubRepositorySync,
- GithubRepository,
- WorkspaceIntegration,
- ProjectMember,
- Label,
- GithubCommentSync,
- Project,
-)
-from plane.api.serializers import (
- GithubIssueSyncSerializer,
- GithubRepositorySyncSerializer,
- GithubCommentSyncSerializer,
-)
-from plane.utils.integrations.github import get_github_repos
-from plane.api.permissions import ProjectBasePermission, ProjectEntityPermission
-
-
-class GithubRepositoriesEndpoint(BaseAPIView):
- permission_classes = [
- ProjectBasePermission,
- ]
-
- def get(self, request, slug, workspace_integration_id):
- try:
- page = request.GET.get("page", 1)
- workspace_integration = WorkspaceIntegration.objects.get(
- workspace__slug=slug, pk=workspace_integration_id
- )
-
- if workspace_integration.integration.provider != "github":
- return Response(
- {"error": "Not a github integration"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- access_tokens_url = workspace_integration.metadata["access_tokens_url"]
- repositories_url = (
- workspace_integration.metadata["repositories_url"]
- + f"?per_page=100&page={page}"
- )
- repositories = get_github_repos(access_tokens_url, repositories_url)
- return Response(repositories, status=status.HTTP_200_OK)
- except WorkspaceIntegration.DoesNotExist:
- return Response(
- {"error": "Workspace Integration Does not exists"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class GithubRepositorySyncViewSet(BaseViewSet):
- permission_classes = [
- ProjectBasePermission,
- ]
-
- serializer_class = GithubRepositorySyncSerializer
- model = GithubRepositorySync
-
- def perform_create(self, serializer):
- serializer.save(project_id=self.kwargs.get("project_id"))
-
- def get_queryset(self):
- return (
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
- .filter(project_id=self.kwargs.get("project_id"))
- )
-
- def create(self, request, slug, project_id, workspace_integration_id):
- try:
- name = request.data.get("name", False)
- url = request.data.get("url", False)
- config = request.data.get("config", {})
- repository_id = request.data.get("repository_id", False)
- owner = request.data.get("owner", False)
-
- if not name or not url or not repository_id or not owner:
- return Response(
- {"error": "Name, url, repository_id and owner are required"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- # Get the workspace integration
- workspace_integration = WorkspaceIntegration.objects.get(
- pk=workspace_integration_id
- )
-
- # Delete the old repository object
- GithubRepositorySync.objects.filter(
- project_id=project_id, workspace__slug=slug
- ).delete()
- GithubRepository.objects.filter(
- project_id=project_id, workspace__slug=slug
- ).delete()
-
- # Create repository
- repo = GithubRepository.objects.create(
- name=name,
- url=url,
- config=config,
- repository_id=repository_id,
- owner=owner,
- project_id=project_id,
- )
-
- # Create a Label for github
- label = Label.objects.filter(
- name="GitHub",
- project_id=project_id,
- ).first()
-
- if label is None:
- label = Label.objects.create(
- name="GitHub",
- project_id=project_id,
- description="Label to sync Plane issues with GitHub issues",
- color="#003773",
- )
-
- # Create repo sync
- repo_sync = GithubRepositorySync.objects.create(
- repository=repo,
- workspace_integration=workspace_integration,
- actor=workspace_integration.actor,
- credentials=request.data.get("credentials", {}),
- project_id=project_id,
- label=label,
- )
-
- # Add bot as a member in the project
- _ = ProjectMember.objects.get_or_create(
- member=workspace_integration.actor, role=20, project_id=project_id
- )
-
- # Return Response
- return Response(
- GithubRepositorySyncSerializer(repo_sync).data,
- status=status.HTTP_201_CREATED,
- )
-
- except WorkspaceIntegration.DoesNotExist:
- return Response(
- {"error": "Workspace Integration does not exist"},
- status=status.HTTP_404_NOT_FOUND,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class GithubIssueSyncViewSet(BaseViewSet):
- permission_classes = [
- ProjectEntityPermission,
- ]
-
- serializer_class = GithubIssueSyncSerializer
- model = GithubIssueSync
-
- def perform_create(self, serializer):
- serializer.save(
- project_id=self.kwargs.get("project_id"),
- repository_sync_id=self.kwargs.get("repo_sync_id"),
- )
-
-
-class BulkCreateGithubIssueSyncEndpoint(BaseAPIView):
- def post(self, request, slug, project_id, repo_sync_id):
- try:
- project = Project.objects.get(pk=project_id, workspace__slug=slug)
-
- github_issue_syncs = request.data.get("github_issue_syncs", [])
- github_issue_syncs = GithubIssueSync.objects.bulk_create(
- [
- GithubIssueSync(
- issue_id=github_issue_sync.get("issue"),
- repo_issue_id=github_issue_sync.get("repo_issue_id"),
- issue_url=github_issue_sync.get("issue_url"),
- github_issue_id=github_issue_sync.get("github_issue_id"),
- repository_sync_id=repo_sync_id,
- project_id=project_id,
- workspace_id=project.workspace_id,
- created_by=request.user,
- updated_by=request.user,
- )
- for github_issue_sync in github_issue_syncs
- ],
- batch_size=100,
- ignore_conflicts=True,
- )
-
- serializer = GithubIssueSyncSerializer(github_issue_syncs, many=True)
- return Response(serializer.data, status=status.HTTP_201_CREATED)
- except Project.DoesNotExist:
- return Response(
- {"error": "Project does not exist"},
- status=status.HTTP_404_NOT_FOUND,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class GithubCommentSyncViewSet(BaseViewSet):
-
- permission_classes = [
- ProjectEntityPermission,
- ]
-
- serializer_class = GithubCommentSyncSerializer
- model = GithubCommentSync
-
- def perform_create(self, serializer):
- serializer.save(
- project_id=self.kwargs.get("project_id"),
- issue_sync_id=self.kwargs.get("issue_sync_id"),
- )
diff --git a/apiserver/plane/api/views/integration/slack.py b/apiserver/plane/api/views/integration/slack.py
deleted file mode 100644
index 498dd0607d..0000000000
--- a/apiserver/plane/api/views/integration/slack.py
+++ /dev/null
@@ -1,73 +0,0 @@
-# Django import
-from django.db import IntegrityError
-
-# Third party imports
-from rest_framework import status
-from rest_framework.response import Response
-from sentry_sdk import capture_exception
-
-# Module imports
-from plane.api.views import BaseViewSet, BaseAPIView
-from plane.db.models import SlackProjectSync, WorkspaceIntegration, ProjectMember
-from plane.api.serializers import SlackProjectSyncSerializer
-from plane.api.permissions import ProjectBasePermission, ProjectEntityPermission
-
-
-class SlackProjectSyncViewSet(BaseViewSet):
- permission_classes = [
- ProjectBasePermission,
- ]
- serializer_class = SlackProjectSyncSerializer
- model = SlackProjectSync
-
- def get_queryset(self):
- return (
- super()
- .get_queryset()
- .filter(
- workspace__slug=self.kwargs.get("slug"),
- project_id=self.kwargs.get("project_id"),
- )
- .filter(project__project_projectmember__member=self.request.user)
- )
-
- def create(self, request, slug, project_id, workspace_integration_id):
- try:
- serializer = SlackProjectSyncSerializer(data=request.data)
-
- workspace_integration = WorkspaceIntegration.objects.get(
- workspace__slug=slug, pk=workspace_integration_id
- )
-
- if serializer.is_valid():
- serializer.save(
- project_id=project_id,
- workspace_integration_id=workspace_integration_id,
- )
-
- workspace_integration = WorkspaceIntegration.objects.get(
- pk=workspace_integration_id, workspace__slug=slug
- )
-
- _ = ProjectMember.objects.get_or_create(
- member=workspace_integration.actor, role=20, project_id=project_id
- )
-
- return Response(serializer.data, status=status.HTTP_200_OK)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- except IntegrityError:
- return Response(
- {"error": "Slack is already enabled for the project"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except WorkspaceIntegration.DoesNotExist:
- return Response(
- {"error": "Workspace Integration does not exist"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- print(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py
index b5a62dd5d9..41745010fa 100644
--- a/apiserver/plane/api/views/issue.py
+++ b/apiserver/plane/api/views/issue.py
@@ -1,157 +1,68 @@
# Python imports
import json
-import random
from itertools import chain
# Django imports
-from django.utils import timezone
+from django.db import IntegrityError
from django.db.models import (
- Prefetch,
OuterRef,
Func,
- F,
Q,
- Count,
+ F,
Case,
+ When,
Value,
CharField,
- When,
- Exists,
Max,
- IntegerField,
+ Exists,
)
from django.core.serializers.json import DjangoJSONEncoder
-from django.utils.decorators import method_decorator
-from django.views.decorators.gzip import gzip_page
-from django.db import IntegrityError
-from django.db import IntegrityError
+from django.utils import timezone
-# Third Party imports
-from rest_framework.response import Response
+# Third party imports
from rest_framework import status
-from rest_framework.parsers import MultiPartParser, FormParser
-from rest_framework.permissions import AllowAny, IsAuthenticated
-from sentry_sdk import capture_exception
+from rest_framework.response import Response
# Module imports
-from . import BaseViewSet, BaseAPIView
-from plane.api.serializers import (
- IssueCreateSerializer,
- IssueActivitySerializer,
- IssueCommentSerializer,
- IssuePropertySerializer,
- LabelSerializer,
- IssueSerializer,
- LabelSerializer,
- IssueFlatSerializer,
- IssueLinkSerializer,
- IssueLiteSerializer,
- IssueAttachmentSerializer,
- IssueSubscriberSerializer,
- ProjectMemberLiteSerializer,
- IssueReactionSerializer,
- CommentReactionSerializer,
- IssueVoteSerializer,
- IssueRelationSerializer,
- RelatedIssueSerializer,
- IssuePublicSerializer,
-)
-from plane.api.permissions import (
+from .base import BaseAPIView, WebhookMixin
+from plane.app.permissions import (
ProjectEntityPermission,
- WorkSpaceAdminPermission,
ProjectMemberPermission,
ProjectLitePermission,
)
from plane.db.models import (
- Project,
Issue,
- IssueActivity,
- IssueComment,
- IssueProperty,
- Label,
- IssueLink,
IssueAttachment,
- State,
- IssueSubscriber,
+ IssueLink,
+ Project,
+ Label,
ProjectMember,
- IssueReaction,
- CommentReaction,
- ProjectDeployBoard,
- IssueVote,
- IssueRelation,
- ProjectPublicMember,
+ IssueComment,
+ IssueActivity,
)
from plane.bgtasks.issue_activites_task import issue_activity
-from plane.utils.grouper import group_results
-from plane.utils.issue_filters import issue_filters
-from plane.bgtasks.export_task import issue_export_task
+from plane.api.serializers import (
+ IssueSerializer,
+ LabelSerializer,
+ IssueLinkSerializer,
+ IssueCommentSerializer,
+ IssueActivitySerializer,
+)
-class IssueViewSet(BaseViewSet):
- def get_serializer_class(self):
- return (
- IssueCreateSerializer
- if self.action in ["create", "update", "partial_update"]
- else IssueSerializer
- )
+class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
+ """
+ This viewset automatically provides `list`, `create`, `retrieve`,
+ `update` and `destroy` actions related to issue.
+
+ """
model = Issue
+ webhook_event = "issue"
permission_classes = [
ProjectEntityPermission,
]
-
- search_fields = [
- "name",
- ]
-
- filterset_fields = [
- "state__name",
- "assignees__id",
- "workspace__id",
- ]
-
- def perform_create(self, serializer):
- serializer.save(project_id=self.kwargs.get("project_id"))
-
- def perform_update(self, serializer):
- requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
- current_instance = (
- self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
- )
- if current_instance is not None:
- issue_activity.delay(
- type="issue.activity.updated",
- requested_data=requested_data,
- actor_id=str(self.request.user.id),
- issue_id=str(self.kwargs.get("pk", None)),
- project_id=str(self.kwargs.get("project_id", None)),
- current_instance=json.dumps(
- IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
- ),
- epoch=int(timezone.now().timestamp())
- )
-
- return super().perform_update(serializer)
-
- def perform_destroy(self, instance):
- current_instance = (
- self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
- )
- if current_instance is not None:
- issue_activity.delay(
- type="issue.activity.deleted",
- requested_data=json.dumps(
- {"issue_id": str(self.kwargs.get("pk", None))}
- ),
- actor_id=str(self.request.user.id),
- issue_id=str(self.kwargs.get("pk", None)),
- project_id=str(self.kwargs.get("project_id", None)),
- current_instance=json.dumps(
- IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
- ),
- epoch=int(timezone.now().timestamp())
- )
- return super().perform_destroy(instance)
+ serializer_class = IssueSerializer
def get_queryset(self):
return (
@@ -169,550 +80,210 @@ class IssueViewSet(BaseViewSet):
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
- .prefetch_related(
- Prefetch(
- "issue_reactions",
- queryset=IssueReaction.objects.select_related("actor"),
- )
- )
- )
+ .order_by(self.kwargs.get("order_by", "-created_at"))
+ ).distinct()
- @method_decorator(gzip_page)
- def list(self, request, slug, project_id):
- try:
- filters = issue_filters(request.query_params, "GET")
-
- # Custom ordering for priority and state
- priority_order = ["urgent", "high", "medium", "low", "none"]
- state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
-
- order_by_param = request.GET.get("order_by", "-created_at")
-
- issue_queryset = (
- self.get_queryset()
- .filter(**filters)
- .annotate(cycle_id=F("issue_cycle__cycle_id"))
- .annotate(module_id=F("issue_module__module_id"))
- .annotate(
- link_count=IssueLink.objects.filter(issue=OuterRef("id"))
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- .annotate(
- attachment_count=IssueAttachment.objects.filter(
- issue=OuterRef("id")
- )
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- )
-
- # Priority Ordering
- if order_by_param == "priority" or order_by_param == "-priority":
- priority_order = (
- priority_order
- if order_by_param == "priority"
- else priority_order[::-1]
- )
- issue_queryset = issue_queryset.annotate(
- priority_order=Case(
- *[
- When(priority=p, then=Value(i))
- for i, p in enumerate(priority_order)
- ],
- output_field=CharField(),
- )
- ).order_by("priority_order")
-
- # State Ordering
- elif order_by_param in [
- "state__name",
- "state__group",
- "-state__name",
- "-state__group",
- ]:
- state_order = (
- state_order
- if order_by_param in ["state__name", "state__group"]
- else state_order[::-1]
- )
- issue_queryset = issue_queryset.annotate(
- state_order=Case(
- *[
- When(state__group=state_group, then=Value(i))
- for i, state_group in enumerate(state_order)
- ],
- default=Value(len(state_order)),
- output_field=CharField(),
- )
- ).order_by("state_order")
- # assignee and label ordering
- elif order_by_param in [
- "labels__name",
- "-labels__name",
- "assignees__first_name",
- "-assignees__first_name",
- ]:
- issue_queryset = issue_queryset.annotate(
- max_values=Max(
- order_by_param[1::]
- if order_by_param.startswith("-")
- else order_by_param
- )
- ).order_by(
- "-max_values" if order_by_param.startswith("-") else "max_values"
- )
- else:
- issue_queryset = issue_queryset.order_by(order_by_param)
-
- issues = IssueLiteSerializer(issue_queryset, many=True).data
-
- ## Grouping the results
- group_by = request.GET.get("group_by", False)
- sub_group_by = request.GET.get("sub_group_by", False)
- if sub_group_by and sub_group_by == group_by:
- return Response(
- {"error": "Group by and sub group by cannot be same"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- if group_by:
- return Response(
- group_results(issues, group_by, sub_group_by), status=status.HTTP_200_OK
- )
-
- return Response(issues, status=status.HTTP_200_OK)
-
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def create(self, request, slug, project_id):
- try:
- project = Project.objects.get(pk=project_id)
-
- serializer = IssueCreateSerializer(
- data=request.data,
- context={
- "project_id": project_id,
- "workspace_id": project.workspace_id,
- "default_assignee_id": project.default_assignee_id,
- },
- )
-
- if serializer.is_valid():
- serializer.save()
-
- # Track the issue
- issue_activity.delay(
- type="issue.activity.created",
- requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
- actor_id=str(request.user.id),
- issue_id=str(serializer.data.get("id", None)),
- project_id=str(project_id),
- current_instance=None,
- epoch=int(timezone.now().timestamp())
- )
- return Response(serializer.data, status=status.HTTP_201_CREATED)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-
- except Project.DoesNotExist:
- return Response(
- {"error": "Project was not found"}, status=status.HTTP_404_NOT_FOUND
- )
-
- def retrieve(self, request, slug, project_id, pk=None):
- try:
+ def get(self, request, slug, project_id, pk=None):
+ if pk:
issue = Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
- ).get(
- workspace__slug=slug, project_id=project_id, pk=pk
- )
- return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
- except Issue.DoesNotExist:
+ ).get(workspace__slug=slug, project_id=project_id, pk=pk)
return Response(
- {"error": "Issue Does not exist"}, status=status.HTTP_404_NOT_FOUND
+ IssueSerializer(
+ issue,
+ fields=self.fields,
+ expand=self.expand,
+ ).data,
+ status=status.HTTP_200_OK,
)
+ # Custom ordering for priority and state
+ priority_order = ["urgent", "high", "medium", "low", "none"]
+ state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
-class UserWorkSpaceIssues(BaseAPIView):
- @method_decorator(gzip_page)
- def get(self, request, slug):
- try:
- filters = issue_filters(request.query_params, "GET")
- # Custom ordering for priority and state
- priority_order = ["urgent", "high", "medium", "low", "none"]
- state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
+ order_by_param = request.GET.get("order_by", "-created_at")
- order_by_param = request.GET.get("order_by", "-created_at")
-
- issue_queryset = (
- Issue.issue_objects.filter(
- (
- Q(assignees__in=[request.user])
- | Q(created_by=request.user)
- | Q(issue_subscribers__subscriber=request.user)
- ),
- workspace__slug=slug,
- )
- .annotate(
- sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- .select_related("project")
- .select_related("workspace")
- .select_related("state")
- .select_related("parent")
- .prefetch_related("assignees")
- .prefetch_related("labels")
- .order_by(order_by_param)
- .annotate(
- link_count=IssueLink.objects.filter(issue=OuterRef("id"))
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- .annotate(
- attachment_count=IssueAttachment.objects.filter(
- issue=OuterRef("id")
- )
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- .prefetch_related(
- Prefetch(
- "issue_reactions",
- queryset=IssueReaction.objects.select_related("actor"),
- )
- )
- .filter(**filters)
- ).distinct()
-
- # Priority Ordering
- if order_by_param == "priority" or order_by_param == "-priority":
- priority_order = (
- priority_order
- if order_by_param == "priority"
- else priority_order[::-1]
- )
- issue_queryset = issue_queryset.annotate(
- priority_order=Case(
- *[
- When(priority=p, then=Value(i))
- for i, p in enumerate(priority_order)
- ],
- output_field=CharField(),
- )
- ).order_by("priority_order")
-
- # State Ordering
- elif order_by_param in [
- "state__name",
- "state__group",
- "-state__name",
- "-state__group",
- ]:
- state_order = (
- state_order
- if order_by_param in ["state__name", "state__group"]
- else state_order[::-1]
- )
- issue_queryset = issue_queryset.annotate(
- state_order=Case(
- *[
- When(state__group=state_group, then=Value(i))
- for i, state_group in enumerate(state_order)
- ],
- default=Value(len(state_order)),
- output_field=CharField(),
- )
- ).order_by("state_order")
- # assignee and label ordering
- elif order_by_param in [
- "labels__name",
- "-labels__name",
- "assignees__first_name",
- "-assignees__first_name",
- ]:
- issue_queryset = issue_queryset.annotate(
- max_values=Max(
- order_by_param[1::]
- if order_by_param.startswith("-")
- else order_by_param
- )
- ).order_by(
- "-max_values" if order_by_param.startswith("-") else "max_values"
- )
- else:
- issue_queryset = issue_queryset.order_by(order_by_param)
-
- issues = IssueLiteSerializer(issue_queryset, many=True).data
-
- ## Grouping the results
- group_by = request.GET.get("group_by", False)
- sub_group_by = request.GET.get("sub_group_by", False)
- if sub_group_by and sub_group_by == group_by:
- return Response(
- {"error": "Group by and sub group by cannot be same"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- if group_by:
- return Response(
- group_results(issues, group_by, sub_group_by), status=status.HTTP_200_OK
- )
-
- return Response(issues, status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class WorkSpaceIssuesEndpoint(BaseAPIView):
- permission_classes = [
- WorkSpaceAdminPermission,
- ]
-
- @method_decorator(gzip_page)
- def get(self, request, slug):
- try:
- issues = (
- Issue.issue_objects.filter(workspace__slug=slug)
- .filter(project__project_projectmember__member=self.request.user)
- .order_by("-created_at")
- )
- serializer = IssueSerializer(issues, many=True)
- return Response(serializer.data, status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class IssueActivityEndpoint(BaseAPIView):
- permission_classes = [
- ProjectEntityPermission,
- ]
-
- @method_decorator(gzip_page)
- def get(self, request, slug, project_id, issue_id):
- try:
- issue_activities = (
- IssueActivity.objects.filter(issue_id=issue_id)
- .filter(
- ~Q(field__in=["comment", "vote", "reaction", "draft"]),
- project__project_projectmember__member=self.request.user,
- )
- .select_related("actor", "workspace", "issue", "project")
- ).order_by("created_at")
- issue_comments = (
- IssueComment.objects.filter(issue_id=issue_id)
- .filter(project__project_projectmember__member=self.request.user)
- .order_by("created_at")
- .select_related("actor", "issue", "project", "workspace")
- .prefetch_related(
- Prefetch(
- "comment_reactions",
- queryset=CommentReaction.objects.select_related("actor"),
- )
- )
- )
- issue_activities = IssueActivitySerializer(issue_activities, many=True).data
- issue_comments = IssueCommentSerializer(issue_comments, many=True).data
-
- result_list = sorted(
- chain(issue_activities, issue_comments),
- key=lambda instance: instance["created_at"],
- )
-
- return Response(result_list, status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class IssueCommentViewSet(BaseViewSet):
- serializer_class = IssueCommentSerializer
- model = IssueComment
- permission_classes = [
- ProjectLitePermission,
- ]
-
- filterset_fields = [
- "issue__id",
- "workspace__id",
- ]
-
- def perform_create(self, serializer):
- serializer.save(
- project_id=self.kwargs.get("project_id"),
- issue_id=self.kwargs.get("issue_id"),
- actor=self.request.user if self.request.user is not None else None,
- )
- issue_activity.delay(
- type="comment.activity.created",
- requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
- actor_id=str(self.request.user.id),
- issue_id=str(self.kwargs.get("issue_id")),
- project_id=str(self.kwargs.get("project_id")),
- current_instance=None,
- epoch=int(timezone.now().timestamp())
- )
-
- def perform_update(self, serializer):
- requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
- current_instance = (
- self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
- )
- if current_instance is not None:
- issue_activity.delay(
- type="comment.activity.updated",
- requested_data=requested_data,
- actor_id=str(self.request.user.id),
- issue_id=str(self.kwargs.get("issue_id", None)),
- project_id=str(self.kwargs.get("project_id", None)),
- current_instance=json.dumps(
- IssueCommentSerializer(current_instance).data,
- cls=DjangoJSONEncoder,
- ),
- epoch=int(timezone.now().timestamp())
- )
-
- return super().perform_update(serializer)
-
- def perform_destroy(self, instance):
- current_instance = (
- self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
- )
- if current_instance is not None:
- issue_activity.delay(
- type="comment.activity.deleted",
- requested_data=json.dumps(
- {"comment_id": str(self.kwargs.get("pk", None))}
- ),
- actor_id=str(self.request.user.id),
- issue_id=str(self.kwargs.get("issue_id", None)),
- project_id=str(self.kwargs.get("project_id", None)),
- current_instance=json.dumps(
- IssueCommentSerializer(current_instance).data,
- cls=DjangoJSONEncoder,
- ),
- epoch=int(timezone.now().timestamp())
- )
- return super().perform_destroy(instance)
-
- def get_queryset(self):
- return self.filter_queryset(
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
- .filter(project_id=self.kwargs.get("project_id"))
- .filter(issue_id=self.kwargs.get("issue_id"))
- .filter(project__project_projectmember__member=self.request.user)
- .select_related("project")
- .select_related("workspace")
- .select_related("issue")
+ issue_queryset = (
+ self.get_queryset()
+ .annotate(cycle_id=F("issue_cycle__cycle_id"))
+ .annotate(module_id=F("issue_module__module_id"))
.annotate(
- is_member=Exists(
- ProjectMember.objects.filter(
- workspace__slug=self.kwargs.get("slug"),
- project_id=self.kwargs.get("project_id"),
- member_id=self.request.user.id,
- )
+ link_count=IssueLink.objects.filter(issue=OuterRef("id"))
+ .order_by()
+ .annotate(count=Func(F("id"), function="Count"))
+ .values("count")
+ )
+ .annotate(
+ attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
+ .order_by()
+ .annotate(count=Func(F("id"), function="Count"))
+ .values("count")
+ )
+ )
+
+ # Priority Ordering
+ if order_by_param == "priority" or order_by_param == "-priority":
+ priority_order = (
+ priority_order if order_by_param == "priority" else priority_order[::-1]
+ )
+ issue_queryset = issue_queryset.annotate(
+ priority_order=Case(
+ *[
+ When(priority=p, then=Value(i))
+ for i, p in enumerate(priority_order)
+ ],
+ output_field=CharField(),
)
+ ).order_by("priority_order")
+
+ # State Ordering
+ elif order_by_param in [
+ "state__name",
+ "state__group",
+ "-state__name",
+ "-state__group",
+ ]:
+ state_order = (
+ state_order
+ if order_by_param in ["state__name", "state__group"]
+ else state_order[::-1]
)
- .distinct()
- )
-
-
-class IssuePropertyViewSet(BaseViewSet):
- serializer_class = IssuePropertySerializer
- model = IssueProperty
- permission_classes = [
- ProjectEntityPermission,
- ]
-
- filterset_fields = []
-
- def perform_create(self, serializer):
- serializer.save(
- project_id=self.kwargs.get("project_id"), user=self.request.user
- )
-
- def get_queryset(self):
- return self.filter_queryset(
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
- .filter(project_id=self.kwargs.get("project_id"))
- .filter(user=self.request.user)
- .filter(project__project_projectmember__member=self.request.user)
- .select_related("project")
- .select_related("workspace")
- )
-
- def list(self, request, slug, project_id):
- queryset = self.get_queryset()
- serializer = IssuePropertySerializer(queryset, many=True)
- return Response(
- serializer.data[0] if len(serializer.data) > 0 else [],
- status=status.HTTP_200_OK,
- )
-
- def create(self, request, slug, project_id):
- try:
- issue_property, created = IssueProperty.objects.get_or_create(
- user=request.user,
- project_id=project_id,
+ issue_queryset = issue_queryset.annotate(
+ state_order=Case(
+ *[
+ When(state__group=state_group, then=Value(i))
+ for i, state_group in enumerate(state_order)
+ ],
+ default=Value(len(state_order)),
+ output_field=CharField(),
+ )
+ ).order_by("state_order")
+ # assignee and label ordering
+ elif order_by_param in [
+ "labels__name",
+ "-labels__name",
+ "assignees__first_name",
+ "-assignees__first_name",
+ ]:
+ issue_queryset = issue_queryset.annotate(
+ max_values=Max(
+ order_by_param[1::]
+ if order_by_param.startswith("-")
+ else order_by_param
+ )
+ ).order_by(
+ "-max_values" if order_by_param.startswith("-") else "max_values"
)
+ else:
+ issue_queryset = issue_queryset.order_by(order_by_param)
- if not created:
- issue_property.properties = request.data.get("properties", {})
- issue_property.save()
+ return self.paginate(
+ request=request,
+ queryset=(issue_queryset),
+ on_results=lambda issues: IssueSerializer(
+ issues,
+ many=True,
+ fields=self.fields,
+ expand=self.expand,
+ ).data,
+ )
- serializer = IssuePropertySerializer(issue_property)
- return Response(serializer.data, status=status.HTTP_200_OK)
+ def post(self, request, slug, project_id):
+ project = Project.objects.get(pk=project_id)
- issue_property.properties = request.data.get("properties", {})
- issue_property.save()
- serializer = IssuePropertySerializer(issue_property)
+ serializer = IssueSerializer(
+ data=request.data,
+ context={
+ "project_id": project_id,
+ "workspace_id": project.workspace_id,
+ "default_assignee_id": project.default_assignee_id,
+ },
+ )
+
+ if serializer.is_valid():
+ serializer.save()
+
+ # Track the issue
+ issue_activity.delay(
+ type="issue.activity.created",
+ requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
+ actor_id=str(request.user.id),
+ issue_id=str(serializer.data.get("id", None)),
+ project_id=str(project_id),
+ current_instance=None,
+ epoch=int(timezone.now().timestamp()),
+ )
return Response(serializer.data, status=status.HTTP_201_CREATED)
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
+ def patch(self, request, slug, project_id, pk=None):
+ issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
+ current_instance = json.dumps(
+ IssueSerializer(issue).data, cls=DjangoJSONEncoder
+ )
+ requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
+ serializer = IssueSerializer(issue, data=request.data, partial=True)
+ if serializer.is_valid():
+ serializer.save()
+ issue_activity.delay(
+ type="issue.activity.updated",
+ requested_data=requested_data,
+ actor_id=str(request.user.id),
+ issue_id=str(pk),
+ project_id=str(project_id),
+ current_instance=current_instance,
+ epoch=int(timezone.now().timestamp()),
)
+ return Response(serializer.data, status=status.HTTP_200_OK)
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+ def delete(self, request, slug, project_id, pk=None):
+ issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
+ current_instance = json.dumps(
+ IssueSerializer(issue).data, cls=DjangoJSONEncoder
+ )
+ issue.delete()
+ issue_activity.delay(
+ type="issue.activity.deleted",
+ requested_data=json.dumps({"issue_id": str(pk)}),
+ actor_id=str(request.user.id),
+ issue_id=str(pk),
+ project_id=str(project_id),
+ current_instance=current_instance,
+ epoch=int(timezone.now().timestamp()),
+ )
+ return Response(status=status.HTTP_204_NO_CONTENT)
-class LabelViewSet(BaseViewSet):
+class LabelAPIEndpoint(BaseAPIView):
+ """
+ This viewset automatically provides `list`, `create`, `retrieve`,
+ `update` and `destroy` actions related to the labels.
+
+ """
+
serializer_class = LabelSerializer
model = Label
permission_classes = [
ProjectMemberPermission,
]
- def create(self, request, slug, project_id):
+ def get_queryset(self):
+ return (
+ Label.objects.filter(workspace__slug=self.kwargs.get("slug"))
+ .filter(project_id=self.kwargs.get("project_id"))
+ .filter(project__project_projectmember__member=self.request.user)
+ .select_related("project")
+ .select_related("workspace")
+ .select_related("parent")
+ .distinct()
+ .order_by(self.kwargs.get("order_by", "-created_at"))
+ )
+
+ def post(self, request, slug, project_id):
try:
serializer = LabelSerializer(data=request.data)
if serializer.is_valid():
@@ -720,175 +291,49 @@ class LabelViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError:
- return Response({"error": "Label with the same name already exists in the project"}, status=status.HTTP_400_BAD_REQUEST)
- except Exception as e:
- capture_exception(e)
- return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST)
-
- def get_queryset(self):
- return self.filter_queryset(
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
- .filter(project_id=self.kwargs.get("project_id"))
- .filter(project__project_projectmember__member=self.request.user)
- .select_related("project")
- .select_related("workspace")
- .select_related("parent")
- .order_by("name")
- .distinct()
- )
-
-
-class BulkDeleteIssuesEndpoint(BaseAPIView):
- permission_classes = [
- ProjectEntityPermission,
- ]
-
- def delete(self, request, slug, project_id):
- try:
- issue_ids = request.data.get("issue_ids", [])
-
- if not len(issue_ids):
- return Response(
- {"error": "Issue IDs are required"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- issues = Issue.issue_objects.filter(
- workspace__slug=slug, project_id=project_id, pk__in=issue_ids
- )
-
- total_issues = len(issues)
-
- issues.delete()
-
return Response(
- {"message": f"{total_issues} issues were deleted"},
- status=status.HTTP_200_OK,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
+ {"error": "Label with the same name already exists in the project"},
status=status.HTTP_400_BAD_REQUEST,
)
-
-class SubIssuesEndpoint(BaseAPIView):
- permission_classes = [
- ProjectEntityPermission,
- ]
-
- @method_decorator(gzip_page)
- def get(self, request, slug, project_id, issue_id):
- try:
- sub_issues = (
- Issue.issue_objects.filter(parent_id=issue_id, workspace__slug=slug)
- .select_related("project")
- .select_related("workspace")
- .select_related("state")
- .select_related("parent")
- .prefetch_related("assignees")
- .prefetch_related("labels")
- .annotate(
- sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- .annotate(
- link_count=IssueLink.objects.filter(issue=OuterRef("id"))
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- .annotate(
- attachment_count=IssueAttachment.objects.filter(
- issue=OuterRef("id")
- )
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- .prefetch_related(
- Prefetch(
- "issue_reactions",
- queryset=IssueReaction.objects.select_related("actor"),
- )
- )
+ def get(self, request, slug, project_id, pk=None):
+ if pk is None:
+ return self.paginate(
+ request=request,
+ queryset=(self.get_queryset()),
+ on_results=lambda labels: LabelSerializer(
+ labels,
+ many=True,
+ fields=self.fields,
+ expand=self.expand,
+ ).data,
)
+ label = self.get_queryset().get(pk=pk)
+ serializer = LabelSerializer(label, fields=self.fields, expand=self.expand,)
+ return Response(serializer.data, status=status.HTTP_200_OK)
- state_distribution = (
- State.objects.filter(
- workspace__slug=slug, state_issue__parent_id=issue_id
- )
- .annotate(state_group=F("group"))
- .values("state_group")
- .annotate(state_count=Count("state_group"))
- .order_by("state_group")
- )
+ def patch(self, request, slug, project_id, pk=None):
+ label = self.get_queryset().get(pk=pk)
+ serializer = LabelSerializer(label, data=request.data, partial=True)
+ if serializer.is_valid():
+ serializer.save()
+ return Response(serializer.data, status=status.HTTP_200_OK)
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
- result = {
- item["state_group"]: item["state_count"] for item in state_distribution
- }
-
- serializer = IssueLiteSerializer(
- sub_issues,
- many=True,
- )
- return Response(
- {
- "sub_issues": serializer.data,
- "state_distribution": result,
- },
- status=status.HTTP_200_OK,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- # Assign multiple sub issues
- def post(self, request, slug, project_id, issue_id):
- try:
- parent_issue = Issue.issue_objects.get(pk=issue_id)
- sub_issue_ids = request.data.get("sub_issue_ids", [])
-
- if not len(sub_issue_ids):
- return Response(
- {"error": "Sub Issue IDs are required"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids)
-
- for sub_issue in sub_issues:
- sub_issue.parent = parent_issue
-
- _ = Issue.objects.bulk_update(sub_issues, ["parent"], batch_size=10)
-
- updated_sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids)
-
- return Response(
- IssueFlatSerializer(updated_sub_issues, many=True).data,
- status=status.HTTP_200_OK,
- )
- except Issue.DoesNotExist:
- return Response(
- {"Parent Issue does not exists"}, status=status.HTTP_400_BAD_REQUEST
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
+ def delete(self, request, slug, project_id, pk=None):
+ label = self.get_queryset().get(pk=pk)
+ label.delete()
+ return Response(status=status.HTTP_204_NO_CONTENT)
-class IssueLinkViewSet(BaseViewSet):
+class IssueLinkAPIEndpoint(BaseAPIView):
+ """
+ This viewset automatically provides `list`, `create`, `retrieve`,
+ `update` and `destroy` actions related to the links of the particular issue.
+
+ """
+
permission_classes = [
ProjectEntityPermission,
]
@@ -896,1758 +341,260 @@ class IssueLinkViewSet(BaseViewSet):
model = IssueLink
serializer_class = IssueLinkSerializer
- def perform_create(self, serializer):
- serializer.save(
- project_id=self.kwargs.get("project_id"),
- issue_id=self.kwargs.get("issue_id"),
- )
- issue_activity.delay(
- type="link.activity.created",
- requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
- actor_id=str(self.request.user.id),
- issue_id=str(self.kwargs.get("issue_id")),
- project_id=str(self.kwargs.get("project_id")),
- current_instance=None,
- epoch=int(timezone.now().timestamp())
+ def get_queryset(self):
+ return (
+ IssueLink.objects.filter(workspace__slug=self.kwargs.get("slug"))
+ .filter(project_id=self.kwargs.get("project_id"))
+ .filter(issue_id=self.kwargs.get("issue_id"))
+ .filter(project__project_projectmember__member=self.request.user)
+ .order_by(self.kwargs.get("order_by", "-created_at"))
+ .distinct()
)
- def perform_update(self, serializer):
- requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
- current_instance = (
- self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
+ def get(self, request, slug, project_id, issue_id, pk=None):
+ if pk is None:
+ issue_links = self.get_queryset()
+ serializer = IssueLinkSerializer(
+ issue_links,
+ fields=self.fields,
+ expand=self.expand,
+ )
+ return self.paginate(
+ request=request,
+ queryset=(self.get_queryset()),
+ on_results=lambda issue_links: IssueLinkSerializer(
+ issue_links,
+ many=True,
+ fields=self.fields,
+ expand=self.expand,
+ ).data,
+ )
+ issue_link = self.get_queryset().get(pk=pk)
+ serializer = IssueLinkSerializer(
+ issue_link,
+ fields=self.fields,
+ expand=self.expand,
)
- if current_instance is not None:
+ return Response(serializer.data, status=status.HTTP_200_OK)
+
+ def post(self, request, slug, project_id, issue_id):
+ serializer = IssueLinkSerializer(data=request.data)
+ if serializer.is_valid():
+ serializer.save(
+ project_id=project_id,
+ issue_id=issue_id,
+ )
+ issue_activity.delay(
+ type="link.activity.created",
+ requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
+ actor_id=str(self.request.user.id),
+ issue_id=str(self.kwargs.get("issue_id")),
+ project_id=str(self.kwargs.get("project_id")),
+ current_instance=None,
+ epoch=int(timezone.now().timestamp()),
+ )
+ return Response(serializer.data, status=status.HTTP_201_CREATED)
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+ def patch(self, request, slug, project_id, issue_id, pk):
+ issue_link = IssueLink.objects.get(
+ workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
+ )
+ requested_data = json.dumps(request.data, cls=DjangoJSONEncoder)
+ current_instance = json.dumps(
+ IssueLinkSerializer(issue_link).data,
+ cls=DjangoJSONEncoder,
+ )
+ serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True)
+ if serializer.is_valid():
+ serializer.save()
issue_activity.delay(
type="link.activity.updated",
requested_data=requested_data,
- actor_id=str(self.request.user.id),
- issue_id=str(self.kwargs.get("issue_id", None)),
- project_id=str(self.kwargs.get("project_id", None)),
- current_instance=json.dumps(
- IssueLinkSerializer(current_instance).data,
- cls=DjangoJSONEncoder,
- ),
- epoch=int(timezone.now().timestamp())
- )
-
- return super().perform_update(serializer)
-
- def perform_destroy(self, instance):
- current_instance = (
- self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
- )
- if current_instance is not None:
- issue_activity.delay(
- type="link.activity.deleted",
- requested_data=json.dumps(
- {"link_id": str(self.kwargs.get("pk", None))}
- ),
- actor_id=str(self.request.user.id),
- issue_id=str(self.kwargs.get("issue_id", None)),
- project_id=str(self.kwargs.get("project_id", None)),
- current_instance=json.dumps(
- IssueLinkSerializer(current_instance).data,
- cls=DjangoJSONEncoder,
- ),
- epoch=int(timezone.now().timestamp())
- )
- return super().perform_destroy(instance)
-
- def get_queryset(self):
- return (
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
- .filter(project_id=self.kwargs.get("project_id"))
- .filter(issue_id=self.kwargs.get("issue_id"))
- .filter(project__project_projectmember__member=self.request.user)
- .order_by("-created_at")
- .distinct()
- )
-
-
-class BulkCreateIssueLabelsEndpoint(BaseAPIView):
- def post(self, request, slug, project_id):
- try:
- label_data = request.data.get("label_data", [])
- project = Project.objects.get(pk=project_id)
-
- labels = Label.objects.bulk_create(
- [
- Label(
- name=label.get("name", "Migrated"),
- description=label.get("description", "Migrated Issue"),
- color="#" + "%06x" % random.randint(0, 0xFFFFFF),
- project_id=project_id,
- workspace_id=project.workspace_id,
- created_by=request.user,
- updated_by=request.user,
- )
- for label in label_data
- ],
- batch_size=50,
- ignore_conflicts=True,
- )
-
- return Response(
- {"labels": LabelSerializer(labels, many=True).data},
- status=status.HTTP_201_CREATED,
- )
- except Project.DoesNotExist:
- return Response(
- {"error": "Project Does not exist"}, status=status.HTTP_404_NOT_FOUND
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class IssueAttachmentEndpoint(BaseAPIView):
- serializer_class = IssueAttachmentSerializer
- permission_classes = [
- ProjectEntityPermission,
- ]
- model = IssueAttachment
- parser_classes = (MultiPartParser, FormParser)
-
- def post(self, request, slug, project_id, issue_id):
- try:
- serializer = IssueAttachmentSerializer(data=request.data)
- if serializer.is_valid():
- serializer.save(project_id=project_id, issue_id=issue_id)
- issue_activity.delay(
- type="attachment.activity.created",
- requested_data=None,
- actor_id=str(self.request.user.id),
- issue_id=str(self.kwargs.get("issue_id", None)),
- project_id=str(self.kwargs.get("project_id", None)),
- current_instance=json.dumps(
- serializer.data,
- cls=DjangoJSONEncoder,
- ),
- epoch=int(timezone.now().timestamp())
- )
- return Response(serializer.data, status=status.HTTP_201_CREATED)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
+ actor_id=str(request.user.id),
+ issue_id=str(issue_id),
+ project_id=str(project_id),
+ current_instance=current_instance,
+ epoch=int(timezone.now().timestamp()),
)
+ return Response(serializer.data, status=status.HTTP_200_OK)
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, slug, project_id, issue_id, pk):
- try:
- issue_attachment = IssueAttachment.objects.get(pk=pk)
- issue_attachment.asset.delete(save=False)
- issue_attachment.delete()
- issue_activity.delay(
- type="attachment.activity.deleted",
- requested_data=None,
- actor_id=str(self.request.user.id),
- issue_id=str(self.kwargs.get("issue_id", None)),
- project_id=str(self.kwargs.get("project_id", None)),
- current_instance=None,
- epoch=int(timezone.now().timestamp())
- )
-
- return Response(status=status.HTTP_204_NO_CONTENT)
- except IssueAttachment.DoesNotExist:
- return Response(
- {"error": "Issue Attachment does not exist"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def get(self, request, slug, project_id, issue_id):
- try:
- issue_attachments = IssueAttachment.objects.filter(
- issue_id=issue_id, workspace__slug=slug, project_id=project_id
- )
- serilaizer = IssueAttachmentSerializer(issue_attachments, many=True)
- return Response(serilaizer.data, status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class IssueArchiveViewSet(BaseViewSet):
- permission_classes = [
- ProjectEntityPermission,
- ]
- serializer_class = IssueFlatSerializer
- model = Issue
-
- def get_queryset(self):
- return (
- Issue.objects.annotate(
- sub_issues_count=Issue.objects.filter(parent=OuterRef("id"))
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- .filter(archived_at__isnull=False)
- .filter(project_id=self.kwargs.get("project_id"))
- .filter(workspace__slug=self.kwargs.get("slug"))
- .select_related("project")
- .select_related("workspace")
- .select_related("state")
- .select_related("parent")
- .prefetch_related("assignees")
- .prefetch_related("labels")
+ issue_link = IssueLink.objects.get(
+ workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
)
-
- @method_decorator(gzip_page)
- def list(self, request, slug, project_id):
- try:
- filters = issue_filters(request.query_params, "GET")
- show_sub_issues = request.GET.get("show_sub_issues", "true")
-
- # Custom ordering for priority and state
- priority_order = ["urgent", "high", "medium", "low", "none"]
- state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
-
- order_by_param = request.GET.get("order_by", "-created_at")
-
- issue_queryset = (
- self.get_queryset()
- .filter(**filters)
- .annotate(cycle_id=F("issue_cycle__cycle_id"))
- .annotate(module_id=F("issue_module__module_id"))
- .annotate(
- link_count=IssueLink.objects.filter(issue=OuterRef("id"))
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- .annotate(
- attachment_count=IssueAttachment.objects.filter(
- issue=OuterRef("id")
- )
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- )
-
- # Priority Ordering
- if order_by_param == "priority" or order_by_param == "-priority":
- priority_order = (
- priority_order
- if order_by_param == "priority"
- else priority_order[::-1]
- )
- issue_queryset = issue_queryset.annotate(
- priority_order=Case(
- *[
- When(priority=p, then=Value(i))
- for i, p in enumerate(priority_order)
- ],
- output_field=CharField(),
- )
- ).order_by("priority_order")
-
- # State Ordering
- elif order_by_param in [
- "state__name",
- "state__group",
- "-state__name",
- "-state__group",
- ]:
- state_order = (
- state_order
- if order_by_param in ["state__name", "state__group"]
- else state_order[::-1]
- )
- issue_queryset = issue_queryset.annotate(
- state_order=Case(
- *[
- When(state__group=state_group, then=Value(i))
- for i, state_group in enumerate(state_order)
- ],
- default=Value(len(state_order)),
- output_field=CharField(),
- )
- ).order_by("state_order")
- # assignee and label ordering
- elif order_by_param in [
- "labels__name",
- "-labels__name",
- "assignees__first_name",
- "-assignees__first_name",
- ]:
- issue_queryset = issue_queryset.annotate(
- max_values=Max(
- order_by_param[1::]
- if order_by_param.startswith("-")
- else order_by_param
- )
- ).order_by(
- "-max_values" if order_by_param.startswith("-") else "max_values"
- )
- else:
- issue_queryset = issue_queryset.order_by(order_by_param)
-
- issue_queryset = (
- issue_queryset
- if show_sub_issues == "true"
- else issue_queryset.filter(parent__isnull=True)
- )
-
- issues = IssueLiteSerializer(issue_queryset, many=True).data
-
- ## Grouping the results
- group_by = request.GET.get("group_by", False)
- if group_by:
- return Response(
- group_results(issues, group_by), status=status.HTTP_200_OK
- )
-
- return Response(issues, status=status.HTTP_200_OK)
-
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def retrieve(self, request, slug, project_id, pk=None):
- try:
- issue = Issue.objects.get(
- workspace__slug=slug,
- project_id=project_id,
- archived_at__isnull=False,
- pk=pk,
- )
- return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
- except Issue.DoesNotExist:
- return Response(
- {"error": "Issue Does not exist"}, status=status.HTTP_404_NOT_FOUND
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def unarchive(self, request, slug, project_id, pk=None):
- try:
- issue = Issue.objects.get(
- workspace__slug=slug,
- project_id=project_id,
- archived_at__isnull=False,
- pk=pk,
- )
- issue.archived_at = None
- issue.save()
- issue_activity.delay(
- type="issue.activity.updated",
- requested_data=json.dumps({"archived_at": None}),
- actor_id=str(request.user.id),
- issue_id=str(issue.id),
- project_id=str(project_id),
- current_instance=None,
- epoch=int(timezone.now().timestamp())
- )
-
- return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
- except Issue.DoesNotExist:
- return Response(
- {"error": "Issue Does not exist"}, status=status.HTTP_404_NOT_FOUND
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong, please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class IssueSubscriberViewSet(BaseViewSet):
- serializer_class = IssueSubscriberSerializer
- model = IssueSubscriber
-
- permission_classes = [
- ProjectEntityPermission,
- ]
-
- def get_permissions(self):
- if self.action in ["subscribe", "unsubscribe", "subscription_status"]:
- self.permission_classes = [
- ProjectLitePermission,
- ]
- else:
- self.permission_classes = [
- ProjectEntityPermission,
- ]
-
- return super(IssueSubscriberViewSet, self).get_permissions()
-
- def perform_create(self, serializer):
- serializer.save(
- project_id=self.kwargs.get("project_id"),
- issue_id=self.kwargs.get("issue_id"),
- )
-
- def get_queryset(self):
- return (
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
- .filter(project_id=self.kwargs.get("project_id"))
- .filter(issue_id=self.kwargs.get("issue_id"))
- .filter(project__project_projectmember__member=self.request.user)
- .order_by("-created_at")
- .distinct()
- )
-
- def list(self, request, slug, project_id, issue_id):
- try:
- members = (
- ProjectMember.objects.filter(
- workspace__slug=slug, project_id=project_id
- )
- .annotate(
- is_subscribed=Exists(
- IssueSubscriber.objects.filter(
- workspace__slug=slug,
- project_id=project_id,
- issue_id=issue_id,
- subscriber=OuterRef("member"),
- )
- )
- )
- .select_related("member")
- )
- serializer = ProjectMemberLiteSerializer(members, many=True)
- return Response(serializer.data, status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": e},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def destroy(self, request, slug, project_id, issue_id, subscriber_id):
- try:
- issue_subscriber = IssueSubscriber.objects.get(
- project=project_id,
- subscriber=subscriber_id,
- workspace__slug=slug,
- issue=issue_id,
- )
- issue_subscriber.delete()
- return Response(
- status=status.HTTP_204_NO_CONTENT,
- )
- except IssueSubscriber.DoesNotExist:
- return Response(
- {"error": "User is not subscribed to this issue"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def subscribe(self, request, slug, project_id, issue_id):
- try:
- if IssueSubscriber.objects.filter(
- issue_id=issue_id,
- subscriber=request.user,
- workspace__slug=slug,
- project=project_id,
- ).exists():
- return Response(
- {"message": "User already subscribed to the issue."},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- subscriber = IssueSubscriber.objects.create(
- issue_id=issue_id,
- subscriber_id=request.user.id,
- project_id=project_id,
- )
- serilaizer = IssueSubscriberSerializer(subscriber)
- return Response(serilaizer.data, status=status.HTTP_201_CREATED)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong, please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def unsubscribe(self, request, slug, project_id, issue_id):
- try:
- issue_subscriber = IssueSubscriber.objects.get(
- project=project_id,
- subscriber=request.user,
- workspace__slug=slug,
- issue=issue_id,
- )
- issue_subscriber.delete()
- return Response(
- status=status.HTTP_204_NO_CONTENT,
- )
- except IssueSubscriber.DoesNotExist:
- return Response(
- {"error": "User subscribed to this issue"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def subscription_status(self, request, slug, project_id, issue_id):
- try:
- issue_subscriber = IssueSubscriber.objects.filter(
- issue=issue_id,
- subscriber=request.user,
- workspace__slug=slug,
- project=project_id,
- ).exists()
- return Response({"subscribed": issue_subscriber}, status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong, please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class IssueReactionViewSet(BaseViewSet):
- serializer_class = IssueReactionSerializer
- model = IssueReaction
- permission_classes = [
- ProjectLitePermission,
- ]
-
- def get_queryset(self):
- return (
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
- .filter(project_id=self.kwargs.get("project_id"))
- .filter(issue_id=self.kwargs.get("issue_id"))
- .filter(project__project_projectmember__member=self.request.user)
- .order_by("-created_at")
- .distinct()
- )
-
- def perform_create(self, serializer):
- serializer.save(
- issue_id=self.kwargs.get("issue_id"),
- project_id=self.kwargs.get("project_id"),
- actor=self.request.user,
+ current_instance = json.dumps(
+ IssueLinkSerializer(issue_link).data,
+ cls=DjangoJSONEncoder,
)
issue_activity.delay(
- type="issue_reaction.activity.created",
- requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
- actor_id=str(self.request.user.id),
- issue_id=str(self.kwargs.get("issue_id", None)),
- project_id=str(self.kwargs.get("project_id", None)),
- current_instance=None,
- epoch=int(timezone.now().timestamp())
+ type="link.activity.deleted",
+ requested_data=json.dumps({"link_id": str(pk)}),
+ actor_id=str(request.user.id),
+ issue_id=str(issue_id),
+ project_id=str(project_id),
+ current_instance=current_instance,
+ epoch=int(timezone.now().timestamp()),
)
-
- def destroy(self, request, slug, project_id, issue_id, reaction_code):
- try:
- issue_reaction = IssueReaction.objects.get(
- workspace__slug=slug,
- project_id=project_id,
- issue_id=issue_id,
- reaction=reaction_code,
- actor=request.user,
- )
- issue_activity.delay(
- type="issue_reaction.activity.deleted",
- requested_data=None,
- actor_id=str(self.request.user.id),
- issue_id=str(self.kwargs.get("issue_id", None)),
- project_id=str(self.kwargs.get("project_id", None)),
- current_instance=json.dumps(
- {
- "reaction": str(reaction_code),
- "identifier": str(issue_reaction.id),
- }
- ),
- epoch=int(timezone.now().timestamp())
- )
- issue_reaction.delete()
- return Response(status=status.HTTP_204_NO_CONTENT)
- except IssueReaction.DoesNotExist:
- return Response(
- {"error": "Issue reaction does not exist"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
+ issue_link.delete()
+ return Response(status=status.HTTP_204_NO_CONTENT)
-class CommentReactionViewSet(BaseViewSet):
- serializer_class = CommentReactionSerializer
- model = CommentReaction
- permission_classes = [
- ProjectLitePermission,
- ]
+class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
+ """
+ This viewset automatically provides `list`, `create`, `retrieve`,
+ `update` and `destroy` actions related to comments of the particular issue.
- def get_queryset(self):
- return (
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
- .filter(project_id=self.kwargs.get("project_id"))
- .filter(comment_id=self.kwargs.get("comment_id"))
- .filter(project__project_projectmember__member=self.request.user)
- .order_by("-created_at")
- .distinct()
- )
+ """
- def perform_create(self, serializer):
- serializer.save(
- actor=self.request.user,
- comment_id=self.kwargs.get("comment_id"),
- project_id=self.kwargs.get("project_id"),
- )
- issue_activity.delay(
- type="comment_reaction.activity.created",
- requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
- actor_id=str(self.request.user.id),
- issue_id=None,
- project_id=str(self.kwargs.get("project_id", None)),
- current_instance=None,
- epoch=int(timezone.now().timestamp())
- )
-
- def destroy(self, request, slug, project_id, comment_id, reaction_code):
- try:
- comment_reaction = CommentReaction.objects.get(
- workspace__slug=slug,
- project_id=project_id,
- comment_id=comment_id,
- reaction=reaction_code,
- actor=request.user,
- )
- issue_activity.delay(
- type="comment_reaction.activity.deleted",
- requested_data=None,
- actor_id=str(self.request.user.id),
- issue_id=None,
- project_id=str(self.kwargs.get("project_id", None)),
- current_instance=json.dumps(
- {
- "reaction": str(reaction_code),
- "identifier": str(comment_reaction.id),
- "comment_id": str(comment_id),
- }
- ),
- epoch=int(timezone.now().timestamp())
- )
- comment_reaction.delete()
- return Response(status=status.HTTP_204_NO_CONTENT)
- except CommentReaction.DoesNotExist:
- return Response(
- {"error": "Comment reaction does not exist"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class IssueCommentPublicViewSet(BaseViewSet):
serializer_class = IssueCommentSerializer
model = IssueComment
-
- filterset_fields = [
- "issue__id",
- "workspace__id",
- ]
-
- def get_permissions(self):
- if self.action in ["list", "retrieve"]:
- self.permission_classes = [
- AllowAny,
- ]
- else:
- self.permission_classes = [
- IsAuthenticated,
- ]
-
- return super(IssueCommentPublicViewSet, self).get_permissions()
-
- def get_queryset(self):
- try:
- project_deploy_board = ProjectDeployBoard.objects.get(
- workspace__slug=self.kwargs.get("slug"),
- project_id=self.kwargs.get("project_id"),
- )
- if project_deploy_board.comments:
- return self.filter_queryset(
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
- .filter(issue_id=self.kwargs.get("issue_id"))
- .filter(access="EXTERNAL")
- .select_related("project")
- .select_related("workspace")
- .select_related("issue")
- .annotate(
- is_member=Exists(
- ProjectMember.objects.filter(
- workspace__slug=self.kwargs.get("slug"),
- project_id=self.kwargs.get("project_id"),
- member_id=self.request.user.id,
- )
- )
- )
- .distinct()
- ).order_by("created_at")
- else:
- return IssueComment.objects.none()
- except ProjectDeployBoard.DoesNotExist:
- return IssueComment.objects.none()
-
- def create(self, request, slug, project_id, issue_id):
- try:
- project_deploy_board = ProjectDeployBoard.objects.get(
- workspace__slug=slug, project_id=project_id
- )
-
- if not project_deploy_board.comments:
- return Response(
- {"error": "Comments are not enabled for this project"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- serializer = IssueCommentSerializer(data=request.data)
- if serializer.is_valid():
- serializer.save(
- project_id=project_id,
- issue_id=issue_id,
- actor=request.user,
- access="EXTERNAL",
- )
- issue_activity.delay(
- type="comment.activity.created",
- requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
- actor_id=str(request.user.id),
- issue_id=str(issue_id),
- project_id=str(project_id),
- current_instance=None,
- epoch=int(timezone.now().timestamp())
- )
- if not ProjectMember.objects.filter(
- project_id=project_id,
- member=request.user,
- ).exists():
- # Add the user for workspace tracking
- _ = ProjectPublicMember.objects.get_or_create(
- project_id=project_id,
- member=request.user,
- )
-
- return Response(serializer.data, status=status.HTTP_201_CREATED)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def partial_update(self, request, slug, project_id, issue_id, pk):
- try:
- project_deploy_board = ProjectDeployBoard.objects.get(
- workspace__slug=slug, project_id=project_id
- )
-
- if not project_deploy_board.comments:
- return Response(
- {"error": "Comments are not enabled for this project"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- comment = IssueComment.objects.get(
- workspace__slug=slug, pk=pk, actor=request.user
- )
- serializer = IssueCommentSerializer(
- comment, data=request.data, partial=True
- )
- if serializer.is_valid():
- serializer.save()
- issue_activity.delay(
- type="comment.activity.updated",
- requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
- actor_id=str(request.user.id),
- issue_id=str(issue_id),
- project_id=str(project_id),
- current_instance=json.dumps(
- IssueCommentSerializer(comment).data,
- cls=DjangoJSONEncoder,
- ),
- epoch=int(timezone.now().timestamp())
- )
- return Response(serializer.data, status=status.HTTP_200_OK)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- except (IssueComment.DoesNotExist, ProjectDeployBoard.DoesNotExist):
- return Response(
- {"error": "IssueComent Does not exists"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def destroy(self, request, slug, project_id, issue_id, pk):
- try:
- project_deploy_board = ProjectDeployBoard.objects.get(
- workspace__slug=slug, project_id=project_id
- )
-
- if not project_deploy_board.comments:
- return Response(
- {"error": "Comments are not enabled for this project"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- comment = IssueComment.objects.get(
- workspace__slug=slug, pk=pk, project_id=project_id, actor=request.user
- )
- issue_activity.delay(
- type="comment.activity.deleted",
- requested_data=json.dumps({"comment_id": str(pk)}),
- actor_id=str(request.user.id),
- issue_id=str(issue_id),
- project_id=str(project_id),
- current_instance=json.dumps(
- IssueCommentSerializer(comment).data,
- cls=DjangoJSONEncoder,
- ),
- epoch=int(timezone.now().timestamp())
- )
- comment.delete()
- return Response(status=status.HTTP_204_NO_CONTENT)
- except (IssueComment.DoesNotExist, ProjectDeployBoard.DoesNotExist):
- return Response(
- {"error": "IssueComent Does not exists"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class IssueReactionPublicViewSet(BaseViewSet):
- serializer_class = IssueReactionSerializer
- model = IssueReaction
-
- def get_queryset(self):
- try:
- project_deploy_board = ProjectDeployBoard.objects.get(
- workspace__slug=self.kwargs.get("slug"),
- project_id=self.kwargs.get("project_id"),
- )
- if project_deploy_board.reactions:
- return (
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
- .filter(project_id=self.kwargs.get("project_id"))
- .filter(issue_id=self.kwargs.get("issue_id"))
- .order_by("-created_at")
- .distinct()
- )
- else:
- return IssueReaction.objects.none()
- except ProjectDeployBoard.DoesNotExist:
- return IssueReaction.objects.none()
-
- def create(self, request, slug, project_id, issue_id):
- try:
- project_deploy_board = ProjectDeployBoard.objects.get(
- workspace__slug=slug, project_id=project_id
- )
-
- if not project_deploy_board.reactions:
- return Response(
- {"error": "Reactions are not enabled for this project board"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- serializer = IssueReactionSerializer(data=request.data)
- if serializer.is_valid():
- serializer.save(
- project_id=project_id, issue_id=issue_id, actor=request.user
- )
- if not ProjectMember.objects.filter(
- project_id=project_id,
- member=request.user,
- ).exists():
- # Add the user for workspace tracking
- _ = ProjectPublicMember.objects.get_or_create(
- project_id=project_id,
- member=request.user,
- )
- issue_activity.delay(
- type="issue_reaction.activity.created",
- requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
- actor_id=str(self.request.user.id),
- issue_id=str(self.kwargs.get("issue_id", None)),
- project_id=str(self.kwargs.get("project_id", None)),
- current_instance=None,
- epoch=int(timezone.now().timestamp())
- )
- return Response(serializer.data, status=status.HTTP_201_CREATED)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- except ProjectDeployBoard.DoesNotExist:
- return Response(
- {"error": "Project board does not exist"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def destroy(self, request, slug, project_id, issue_id, reaction_code):
- try:
- project_deploy_board = ProjectDeployBoard.objects.get(
- workspace__slug=slug, project_id=project_id
- )
-
- if not project_deploy_board.reactions:
- return Response(
- {"error": "Reactions are not enabled for this project board"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- issue_reaction = IssueReaction.objects.get(
- workspace__slug=slug,
- issue_id=issue_id,
- reaction=reaction_code,
- actor=request.user,
- )
- issue_activity.delay(
- type="issue_reaction.activity.deleted",
- requested_data=None,
- actor_id=str(self.request.user.id),
- issue_id=str(self.kwargs.get("issue_id", None)),
- project_id=str(self.kwargs.get("project_id", None)),
- current_instance=json.dumps(
- {
- "reaction": str(reaction_code),
- "identifier": str(issue_reaction.id),
- }
- ),
- epoch=int(timezone.now().timestamp())
- )
- issue_reaction.delete()
- return Response(status=status.HTTP_204_NO_CONTENT)
- except IssueReaction.DoesNotExist:
- return Response(
- {"error": "Issue reaction does not exist"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class CommentReactionPublicViewSet(BaseViewSet):
- serializer_class = CommentReactionSerializer
- model = CommentReaction
-
- def get_queryset(self):
- try:
- project_deploy_board = ProjectDeployBoard.objects.get(
- workspace__slug=self.kwargs.get("slug"),
- project_id=self.kwargs.get("project_id"),
- )
- if project_deploy_board.reactions:
- return (
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
- .filter(project_id=self.kwargs.get("project_id"))
- .filter(comment_id=self.kwargs.get("comment_id"))
- .order_by("-created_at")
- .distinct()
- )
- else:
- return CommentReaction.objects.none()
- except ProjectDeployBoard.DoesNotExist:
- return CommentReaction.objects.none()
-
- def create(self, request, slug, project_id, comment_id):
- try:
- project_deploy_board = ProjectDeployBoard.objects.get(
- workspace__slug=slug, project_id=project_id
- )
-
- if not project_deploy_board.reactions:
- return Response(
- {"error": "Reactions are not enabled for this board"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- serializer = CommentReactionSerializer(data=request.data)
- if serializer.is_valid():
- serializer.save(
- project_id=project_id, comment_id=comment_id, actor=request.user
- )
- if not ProjectMember.objects.filter(
- project_id=project_id, member=request.user
- ).exists():
- # Add the user for workspace tracking
- _ = ProjectPublicMember.objects.get_or_create(
- project_id=project_id,
- member=request.user,
- )
- issue_activity.delay(
- type="comment_reaction.activity.created",
- requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
- actor_id=str(self.request.user.id),
- issue_id=None,
- project_id=str(self.kwargs.get("project_id", None)),
- current_instance=None,
- epoch=int(timezone.now().timestamp())
- )
- return Response(serializer.data, status=status.HTTP_201_CREATED)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- except IssueComment.DoesNotExist:
- return Response(
- {"error": "Comment does not exist"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except ProjectDeployBoard.DoesNotExist:
- return Response(
- {"error": "Project board does not exist"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def destroy(self, request, slug, project_id, comment_id, reaction_code):
- try:
- project_deploy_board = ProjectDeployBoard.objects.get(
- workspace__slug=slug, project_id=project_id
- )
- if not project_deploy_board.reactions:
- return Response(
- {"error": "Reactions are not enabled for this board"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- comment_reaction = CommentReaction.objects.get(
- project_id=project_id,
- workspace__slug=slug,
- comment_id=comment_id,
- reaction=reaction_code,
- actor=request.user,
- )
- issue_activity.delay(
- type="comment_reaction.activity.deleted",
- requested_data=None,
- actor_id=str(self.request.user.id),
- issue_id=None,
- project_id=str(self.kwargs.get("project_id", None)),
- current_instance=json.dumps(
- {
- "reaction": str(reaction_code),
- "identifier": str(comment_reaction.id),
- "comment_id": str(comment_id),
- }
- ),
- epoch=int(timezone.now().timestamp())
- )
- comment_reaction.delete()
- return Response(status=status.HTTP_204_NO_CONTENT)
- except CommentReaction.DoesNotExist:
- return Response(
- {"error": "Comment reaction does not exist"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class IssueVotePublicViewSet(BaseViewSet):
- model = IssueVote
- serializer_class = IssueVoteSerializer
-
- def get_queryset(self):
- try:
- project_deploy_board = ProjectDeployBoard.objects.get(
- workspace__slug=self.kwargs.get("slug"),
- project_id=self.kwargs.get("project_id"),
- )
- if project_deploy_board.votes:
- return (
- super()
- .get_queryset()
- .filter(issue_id=self.kwargs.get("issue_id"))
- .filter(workspace__slug=self.kwargs.get("slug"))
- .filter(project_id=self.kwargs.get("project_id"))
- )
- else:
- return IssueVote.objects.none()
- except ProjectDeployBoard.DoesNotExist:
- return IssueVote.objects.none()
-
- def create(self, request, slug, project_id, issue_id):
- try:
- issue_vote, _ = IssueVote.objects.get_or_create(
- actor_id=request.user.id,
- project_id=project_id,
- issue_id=issue_id,
- )
- # Add the user for workspace tracking
- if not ProjectMember.objects.filter(
- project_id=project_id, member=request.user
- ).exists():
- _ = ProjectPublicMember.objects.get_or_create(
- project_id=project_id,
- member=request.user,
- )
- issue_vote.vote = request.data.get("vote", 1)
- issue_vote.save()
- issue_activity.delay(
- type="issue_vote.activity.created",
- requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
- actor_id=str(self.request.user.id),
- issue_id=str(self.kwargs.get("issue_id", None)),
- project_id=str(self.kwargs.get("project_id", None)),
- current_instance=None,
- epoch=int(timezone.now().timestamp())
- )
- serializer = IssueVoteSerializer(issue_vote)
- return Response(serializer.data, status=status.HTTP_201_CREATED)
- except IntegrityError:
- return Response(
- {"error": "Reaction already exists"}, status=status.HTTP_400_BAD_REQUEST
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def destroy(self, request, slug, project_id, issue_id):
- try:
- issue_vote = IssueVote.objects.get(
- workspace__slug=slug,
- project_id=project_id,
- issue_id=issue_id,
- actor_id=request.user.id,
- )
- issue_activity.delay(
- type="issue_vote.activity.deleted",
- requested_data=None,
- actor_id=str(self.request.user.id),
- issue_id=str(self.kwargs.get("issue_id", None)),
- project_id=str(self.kwargs.get("project_id", None)),
- current_instance=json.dumps(
- {
- "vote": str(issue_vote.vote),
- "identifier": str(issue_vote.id),
- }
- ),
- epoch=int(timezone.now().timestamp())
- )
- issue_vote.delete()
- return Response(status=status.HTTP_204_NO_CONTENT)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class IssueRelationViewSet(BaseViewSet):
- serializer_class = IssueRelationSerializer
- model = IssueRelation
+ webhook_event = "issue_comment"
permission_classes = [
- ProjectEntityPermission,
+ ProjectLitePermission,
]
- def perform_destroy(self, instance):
- current_instance = (
- self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
- )
- if current_instance is not None:
- issue_activity.delay(
- type="issue_relation.activity.deleted",
- requested_data=json.dumps({"related_list": None}),
- actor_id=str(self.request.user.id),
- issue_id=str(self.kwargs.get("issue_id", None)),
- project_id=str(self.kwargs.get("project_id", None)),
- current_instance=json.dumps(
- IssueRelationSerializer(current_instance).data,
- cls=DjangoJSONEncoder,
- ),
- epoch=int(timezone.now().timestamp())
- )
- return super().perform_destroy(instance)
-
- def create(self, request, slug, project_id, issue_id):
- try:
- related_list = request.data.get("related_list", [])
- relation = request.data.get("relation", None)
- project = Project.objects.get(pk=project_id)
-
- issue_relation = IssueRelation.objects.bulk_create(
- [
- IssueRelation(
- issue_id=related_issue["issue"],
- related_issue_id=related_issue["related_issue"],
- relation_type=related_issue["relation_type"],
- project_id=project_id,
- workspace_id=project.workspace_id,
- created_by=request.user,
- updated_by=request.user,
- )
- for related_issue in related_list
- ],
- batch_size=10,
- ignore_conflicts=True,
- )
-
- issue_activity.delay(
- type="issue_relation.activity.created",
- requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
- actor_id=str(request.user.id),
- issue_id=str(issue_id),
- project_id=str(project_id),
- current_instance=None,
- epoch=int(timezone.now().timestamp())
- )
-
- if relation == "blocking":
- return Response(
- RelatedIssueSerializer(issue_relation, many=True).data,
- status=status.HTTP_201_CREATED,
- )
- else:
- return Response(
- IssueRelationSerializer(issue_relation, many=True).data,
- status=status.HTTP_201_CREATED,
- )
- except IntegrityError as e:
- if "already exists" in str(e):
- return Response(
- {"name": "The issue is already taken"},
- status=status.HTTP_410_GONE,
- )
- else:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
def get_queryset(self):
- return self.filter_queryset(
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
+ return (
+ IssueComment.objects.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(issue_id=self.kwargs.get("issue_id"))
.filter(project__project_projectmember__member=self.request.user)
.select_related("project")
.select_related("workspace")
.select_related("issue")
+ .select_related("actor")
+ .annotate(
+ is_member=Exists(
+ ProjectMember.objects.filter(
+ workspace__slug=self.kwargs.get("slug"),
+ project_id=self.kwargs.get("project_id"),
+ member_id=self.request.user.id,
+ is_active=True,
+ )
+ )
+ )
+ .order_by(self.kwargs.get("order_by", "-created_at"))
.distinct()
)
-
-class IssueRetrievePublicEndpoint(BaseAPIView):
- permission_classes = [
- AllowAny,
- ]
-
- def get(self, request, slug, project_id, issue_id):
- try:
- issue = Issue.objects.get(
- workspace__slug=slug, project_id=project_id, pk=issue_id
+ def get(self, request, slug, project_id, issue_id, pk=None):
+ if pk:
+ issue_comment = self.get_queryset().get(pk=pk)
+ serializer = IssueCommentSerializer(
+ issue_comment,
+ fields=self.fields,
+ expand=self.expand,
)
- serializer = IssuePublicSerializer(issue)
return Response(serializer.data, status=status.HTTP_200_OK)
- except Issue.DoesNotExist:
- return Response(
- {"error": "Issue Does not exist"}, status=status.HTTP_400_BAD_REQUEST
+ return self.paginate(
+ request=request,
+ queryset=(self.get_queryset()),
+ on_results=lambda issue_comment: IssueCommentSerializer(
+ issue_comment,
+ many=True,
+ fields=self.fields,
+ expand=self.expand,
+ ).data,
+ )
+
+ def post(self, request, slug, project_id, issue_id):
+ serializer = IssueCommentSerializer(data=request.data)
+ if serializer.is_valid():
+ serializer.save(
+ project_id=project_id,
+ issue_id=issue_id,
+ actor=request.user,
)
- except Exception as e:
- print(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
+ issue_activity.delay(
+ type="comment.activity.created",
+ requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
+ actor_id=str(self.request.user.id),
+ issue_id=str(self.kwargs.get("issue_id")),
+ project_id=str(self.kwargs.get("project_id")),
+ current_instance=None,
+ epoch=int(timezone.now().timestamp()),
)
+ return Response(serializer.data, status=status.HTTP_201_CREATED)
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-
-class ProjectIssuesPublicEndpoint(BaseAPIView):
- permission_classes = [
- AllowAny,
- ]
-
- def get(self, request, slug, project_id):
- try:
- project_deploy_board = ProjectDeployBoard.objects.get(
- workspace__slug=slug, project_id=project_id
+ def patch(self, request, slug, project_id, issue_id, pk):
+ issue_comment = IssueComment.objects.get(
+ workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
+ )
+ requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
+ current_instance = json.dumps(
+ IssueCommentSerializer(issue_comment).data,
+ cls=DjangoJSONEncoder,
+ )
+ serializer = IssueCommentSerializer(
+ issue_comment, data=request.data, partial=True
+ )
+ if serializer.is_valid():
+ serializer.save()
+ issue_activity.delay(
+ type="comment.activity.updated",
+ requested_data=requested_data,
+ actor_id=str(request.user.id),
+ issue_id=str(issue_id),
+ project_id=str(project_id),
+ current_instance=current_instance,
+ epoch=int(timezone.now().timestamp()),
)
+ return Response(serializer.data, status=status.HTTP_200_OK)
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- filters = issue_filters(request.query_params, "GET")
-
- # Custom ordering for priority and state
- priority_order = ["urgent", "high", "medium", "low", "none"]
- state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
-
- order_by_param = request.GET.get("order_by", "-created_at")
-
- issue_queryset = (
- Issue.issue_objects.annotate(
- sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- .filter(project_id=project_id)
- .filter(workspace__slug=slug)
- .select_related("project", "workspace", "state", "parent")
- .prefetch_related("assignees", "labels")
- .prefetch_related(
- Prefetch(
- "issue_reactions",
- queryset=IssueReaction.objects.select_related("actor"),
- )
- )
- .prefetch_related(
- Prefetch(
- "votes",
- queryset=IssueVote.objects.select_related("actor"),
- )
- )
- .filter(**filters)
- .annotate(cycle_id=F("issue_cycle__cycle_id"))
- .annotate(module_id=F("issue_module__module_id"))
- .annotate(
- link_count=IssueLink.objects.filter(issue=OuterRef("id"))
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- .annotate(
- attachment_count=IssueAttachment.objects.filter(
- issue=OuterRef("id")
- )
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- )
-
- # Priority Ordering
- if order_by_param == "priority" or order_by_param == "-priority":
- priority_order = (
- priority_order
- if order_by_param == "priority"
- else priority_order[::-1]
- )
- issue_queryset = issue_queryset.annotate(
- priority_order=Case(
- *[
- When(priority=p, then=Value(i))
- for i, p in enumerate(priority_order)
- ],
- output_field=CharField(),
- )
- ).order_by("priority_order")
-
- # State Ordering
- elif order_by_param in [
- "state__name",
- "state__group",
- "-state__name",
- "-state__group",
- ]:
- state_order = (
- state_order
- if order_by_param in ["state__name", "state__group"]
- else state_order[::-1]
- )
- issue_queryset = issue_queryset.annotate(
- state_order=Case(
- *[
- When(state__group=state_group, then=Value(i))
- for i, state_group in enumerate(state_order)
- ],
- default=Value(len(state_order)),
- output_field=CharField(),
- )
- ).order_by("state_order")
- # assignee and label ordering
- elif order_by_param in [
- "labels__name",
- "-labels__name",
- "assignees__first_name",
- "-assignees__first_name",
- ]:
- issue_queryset = issue_queryset.annotate(
- max_values=Max(
- order_by_param[1::]
- if order_by_param.startswith("-")
- else order_by_param
- )
- ).order_by(
- "-max_values" if order_by_param.startswith("-") else "max_values"
- )
- else:
- issue_queryset = issue_queryset.order_by(order_by_param)
-
- issues = IssuePublicSerializer(issue_queryset, many=True).data
-
- state_group_order = [
- "backlog",
- "unstarted",
- "started",
- "completed",
- "cancelled",
- ]
-
- states = (
- State.objects.filter(
- ~Q(name="Triage"),
- workspace__slug=slug,
- project_id=project_id,
- )
- .annotate(
- custom_order=Case(
- *[
- When(group=value, then=Value(index))
- for index, value in enumerate(state_group_order)
- ],
- default=Value(len(state_group_order)),
- output_field=IntegerField(),
- ),
- )
- .values("name", "group", "color", "id")
- .order_by("custom_order", "sequence")
- )
-
- labels = Label.objects.filter(
- workspace__slug=slug, project_id=project_id
- ).values("id", "name", "color", "parent")
-
- ## Grouping the results
- group_by = request.GET.get("group_by", False)
- if group_by:
- issues = group_results(issues, group_by)
-
- return Response(
- {
- "issues": issues,
- "states": states,
- "labels": labels,
- },
- status=status.HTTP_200_OK,
- )
- except ProjectDeployBoard.DoesNotExist:
- return Response(
- {"error": "Board does not exists"}, status=status.HTTP_404_NOT_FOUND
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
+ def delete(self, request, slug, project_id, issue_id, pk):
+ issue_comment = IssueComment.objects.get(
+ workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
+ )
+ current_instance = json.dumps(
+ IssueCommentSerializer(issue_comment).data,
+ cls=DjangoJSONEncoder,
+ )
+ issue_comment.delete()
+ issue_activity.delay(
+ type="comment.activity.deleted",
+ requested_data=json.dumps({"comment_id": str(pk)}),
+ actor_id=str(request.user.id),
+ issue_id=str(issue_id),
+ project_id=str(project_id),
+ current_instance=current_instance,
+ epoch=int(timezone.now().timestamp()),
+ )
+ return Response(status=status.HTTP_204_NO_CONTENT)
-class IssueDraftViewSet(BaseViewSet):
+class IssueActivityAPIEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
- serializer_class = IssueFlatSerializer
- model = Issue
-
- def perform_destroy(self, instance):
- current_instance = (
- self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
+ def get(self, request, slug, project_id, issue_id, pk=None):
+ issue_activities = (
+ IssueActivity.objects.filter(
+ issue_id=issue_id, workspace__slug=slug, project_id=project_id
+ )
+ .filter(
+ ~Q(field__in=["comment", "vote", "reaction", "draft"]),
+ project__project_projectmember__member=self.request.user,
+ )
+ .select_related("actor", "workspace", "issue", "project")
+ ).order_by(request.GET.get("order_by", "created_at"))
+
+ if pk:
+ issue_activities = issue_activities.get(pk=pk)
+ serializer = IssueActivitySerializer(issue_activities)
+ return Response(serializer.data, status=status.HTTP_200_OK)
+
+ return self.paginate(
+ request=request,
+ queryset=(issue_activities),
+ on_results=lambda issue_activity: IssueActivitySerializer(
+ issue_activity,
+ many=True,
+ fields=self.fields,
+ expand=self.expand,
+ ).data,
)
- if current_instance is not None:
- issue_activity.delay(
- type="issue_draft.activity.deleted",
- requested_data=json.dumps(
- {"issue_id": str(self.kwargs.get("pk", None))}
- ),
- actor_id=str(self.request.user.id),
- issue_id=str(self.kwargs.get("pk", None)),
- project_id=str(self.kwargs.get("project_id", None)),
- current_instance=json.dumps(
- IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
- ),
- epoch=int(timezone.now().timestamp())
- )
- return super().perform_destroy(instance)
-
-
- def get_queryset(self):
- return (
- Issue.objects.annotate(
- sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- .filter(project_id=self.kwargs.get("project_id"))
- .filter(workspace__slug=self.kwargs.get("slug"))
- .filter(is_draft=True)
- .select_related("project")
- .select_related("workspace")
- .select_related("state")
- .select_related("parent")
- .prefetch_related("assignees")
- .prefetch_related("labels")
- .prefetch_related(
- Prefetch(
- "issue_reactions",
- queryset=IssueReaction.objects.select_related("actor"),
- )
- )
- )
-
-
- @method_decorator(gzip_page)
- def list(self, request, slug, project_id):
- try:
- filters = issue_filters(request.query_params, "GET")
-
- # Custom ordering for priority and state
- priority_order = ["urgent", "high", "medium", "low", "none"]
- state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
-
- order_by_param = request.GET.get("order_by", "-created_at")
-
- issue_queryset = (
- self.get_queryset()
- .filter(**filters)
- .annotate(cycle_id=F("issue_cycle__cycle_id"))
- .annotate(module_id=F("issue_module__module_id"))
- .annotate(
- link_count=IssueLink.objects.filter(issue=OuterRef("id"))
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- .annotate(
- attachment_count=IssueAttachment.objects.filter(
- issue=OuterRef("id")
- )
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- )
-
- # Priority Ordering
- if order_by_param == "priority" or order_by_param == "-priority":
- priority_order = (
- priority_order
- if order_by_param == "priority"
- else priority_order[::-1]
- )
- issue_queryset = issue_queryset.annotate(
- priority_order=Case(
- *[
- When(priority=p, then=Value(i))
- for i, p in enumerate(priority_order)
- ],
- output_field=CharField(),
- )
- ).order_by("priority_order")
-
- # State Ordering
- elif order_by_param in [
- "state__name",
- "state__group",
- "-state__name",
- "-state__group",
- ]:
- state_order = (
- state_order
- if order_by_param in ["state__name", "state__group"]
- else state_order[::-1]
- )
- issue_queryset = issue_queryset.annotate(
- state_order=Case(
- *[
- When(state__group=state_group, then=Value(i))
- for i, state_group in enumerate(state_order)
- ],
- default=Value(len(state_order)),
- output_field=CharField(),
- )
- ).order_by("state_order")
- # assignee and label ordering
- elif order_by_param in [
- "labels__name",
- "-labels__name",
- "assignees__first_name",
- "-assignees__first_name",
- ]:
- issue_queryset = issue_queryset.annotate(
- max_values=Max(
- order_by_param[1::]
- if order_by_param.startswith("-")
- else order_by_param
- )
- ).order_by(
- "-max_values" if order_by_param.startswith("-") else "max_values"
- )
- else:
- issue_queryset = issue_queryset.order_by(order_by_param)
-
- issues = IssueLiteSerializer(issue_queryset, many=True).data
-
- ## Grouping the results
- group_by = request.GET.get("group_by", False)
- if group_by:
- return Response(
- group_results(issues, group_by), status=status.HTTP_200_OK
- )
-
- return Response(issues, status=status.HTTP_200_OK)
-
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
- def create(self, request, slug, project_id):
- try:
- project = Project.objects.get(pk=project_id)
-
- serializer = IssueCreateSerializer(
- data=request.data,
- context={
- "project_id": project_id,
- "workspace_id": project.workspace_id,
- "default_assignee_id": project.default_assignee_id,
- },
- )
-
- if serializer.is_valid():
- serializer.save(is_draft=True)
-
- # Track the issue
- issue_activity.delay(
- type="issue_draft.activity.created",
- requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
- actor_id=str(request.user.id),
- issue_id=str(serializer.data.get("id", None)),
- project_id=str(project_id),
- current_instance=None,
- epoch=int(timezone.now().timestamp())
- )
- return Response(serializer.data, status=status.HTTP_201_CREATED)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-
- except Project.DoesNotExist:
- return Response(
- {"error": "Project was not found"}, status=status.HTTP_404_NOT_FOUND
- )
-
-
- def partial_update(self, request, slug, project_id, pk):
- try:
- issue = Issue.objects.get(
- workspace__slug=slug, project_id=project_id, pk=pk
- )
- serializer = IssueSerializer(
- issue, data=request.data, partial=True
- )
-
- if serializer.is_valid():
- if(request.data.get("is_draft") is not None and not request.data.get("is_draft")):
- serializer.save(created_at=timezone.now(), updated_at=timezone.now())
- else:
- serializer.save()
- issue_activity.delay(
- type="issue_draft.activity.updated",
- requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
- actor_id=str(self.request.user.id),
- issue_id=str(self.kwargs.get("pk", None)),
- project_id=str(self.kwargs.get("project_id", None)),
- current_instance=json.dumps(
- IssueSerializer(issue).data,
- cls=DjangoJSONEncoder,
- ),
- epoch=int(timezone.now().timestamp())
- )
- return Response(serializer.data, status=status.HTTP_200_OK)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- except Issue.DoesNotExist:
- return Response(
- {"error": "Issue does not exists"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
- def retrieve(self, request, slug, project_id, pk=None):
- try:
- issue = Issue.objects.get(
- workspace__slug=slug, project_id=project_id, pk=pk, is_draft=True
- )
- return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
- except Issue.DoesNotExist:
- return Response(
- {"error": "Issue Does not exist"}, status=status.HTTP_404_NOT_FOUND
- )
-
diff --git a/apiserver/plane/api/views/module.py b/apiserver/plane/api/views/module.py
index 1489edb2d5..221c7f31bf 100644
--- a/apiserver/plane/api/views/module.py
+++ b/apiserver/plane/api/views/module.py
@@ -1,74 +1,53 @@
# Python imports
import json
-# Django Imports
+# Django imports
+from django.db.models import Count, Prefetch, Q, F, Func, OuterRef
from django.utils import timezone
-from django.db import IntegrityError
-from django.db.models import Prefetch, F, OuterRef, Func, Exists, Count, Q
from django.core import serializers
-from django.utils.decorators import method_decorator
-from django.views.decorators.gzip import gzip_page
# Third party imports
-from rest_framework.response import Response
from rest_framework import status
-from sentry_sdk import capture_exception
+from rest_framework.response import Response
# Module imports
-from . import BaseViewSet
+from .base import BaseAPIView, WebhookMixin
+from plane.app.permissions import ProjectEntityPermission
+from plane.db.models import (
+ Project,
+ Module,
+ ModuleLink,
+ Issue,
+ ModuleIssue,
+ IssueAttachment,
+ IssueLink,
+)
from plane.api.serializers import (
- ModuleWriteSerializer,
ModuleSerializer,
ModuleIssueSerializer,
- ModuleLinkSerializer,
- ModuleFavoriteSerializer,
- IssueStateSerializer,
-)
-from plane.api.permissions import ProjectEntityPermission
-from plane.db.models import (
- Module,
- ModuleIssue,
- Project,
- Issue,
- ModuleLink,
- ModuleFavorite,
- IssueLink,
- IssueAttachment,
+ IssueSerializer,
)
from plane.bgtasks.issue_activites_task import issue_activity
-from plane.utils.grouper import group_results
-from plane.utils.issue_filters import issue_filters
-from plane.utils.analytics_plot import burndown_plot
-class ModuleViewSet(BaseViewSet):
+class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
+ """
+ This viewset automatically provides `list`, `create`, `retrieve`,
+ `update` and `destroy` actions related to module.
+
+ """
+
model = Module
permission_classes = [
ProjectEntityPermission,
]
-
- def get_serializer_class(self):
- return (
- ModuleWriteSerializer
- if self.action in ["create", "update", "partial_update"]
- else ModuleSerializer
- )
+ serializer_class = ModuleSerializer
+ webhook_event = "module"
def get_queryset(self):
- order_by = self.request.GET.get("order_by", "sort_order")
-
- subquery = ModuleFavorite.objects.filter(
- user=self.request.user,
- module_id=OuterRef("pk"),
- project_id=self.kwargs.get("project_id"),
- workspace__slug=self.kwargs.get("slug"),
- )
return (
- super()
- .get_queryset()
- .filter(project_id=self.kwargs.get("project_id"))
+ Module.objects.filter(project_id=self.kwargs.get("project_id"))
.filter(workspace__slug=self.kwargs.get("slug"))
- .annotate(is_favorite=Exists(subquery))
.select_related("project")
.select_related("workspace")
.select_related("lead")
@@ -138,219 +117,93 @@ class ModuleViewSet(BaseViewSet):
),
)
)
- .order_by(order_by, "name")
+ .order_by(self.kwargs.get("order_by", "-created_at"))
)
- def perform_destroy(self, instance):
- module_issues = list(
- ModuleIssue.objects.filter(module_id=self.kwargs.get("pk")).values_list(
- "issue", flat=True
+ def post(self, request, slug, project_id):
+ project = Project.objects.get(workspace__slug=slug, pk=project_id)
+ serializer = ModuleSerializer(data=request.data, context={"project": project})
+ if serializer.is_valid():
+ serializer.save()
+ module = Module.objects.get(pk=serializer.data["id"])
+ serializer = ModuleSerializer(module)
+ return Response(serializer.data, status=status.HTTP_201_CREATED)
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+ def patch(self, request, slug, project_id, pk):
+ module = Module.objects.get(pk=pk, project_id=project_id, workspace__slug=slug)
+ serializer = ModuleSerializer(module, data=request.data)
+ if serializer.is_valid():
+ serializer.save()
+ return Response(serializer.data, status=status.HTTP_201_CREATED)
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+ def get(self, request, slug, project_id, pk=None):
+ if pk:
+ queryset = self.get_queryset().get(pk=pk)
+ data = ModuleSerializer(
+ queryset,
+ fields=self.fields,
+ expand=self.expand,
+ ).data
+ return Response(
+ data,
+ status=status.HTTP_200_OK,
)
+ return self.paginate(
+ request=request,
+ queryset=(self.get_queryset()),
+ on_results=lambda modules: ModuleSerializer(
+ modules,
+ many=True,
+ fields=self.fields,
+ expand=self.expand,
+ ).data,
+ )
+
+ def delete(self, request, slug, project_id, pk):
+ module = Module.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
+ module_issues = list(
+ ModuleIssue.objects.filter(module_id=pk).values_list("issue", flat=True)
)
issue_activity.delay(
type="module.activity.deleted",
requested_data=json.dumps(
{
- "module_id": str(self.kwargs.get("pk")),
+ "module_id": str(pk),
+ "module_name": str(module.name),
"issues": [str(issue_id) for issue_id in module_issues],
}
),
- actor_id=str(self.request.user.id),
- issue_id=str(self.kwargs.get("pk", None)),
- project_id=str(self.kwargs.get("project_id", None)),
+ actor_id=str(request.user.id),
+ issue_id=None,
+ project_id=str(project_id),
current_instance=None,
- epoch=int(timezone.now().timestamp())
+ epoch=int(timezone.now().timestamp()),
)
-
- return super().perform_destroy(instance)
-
- def create(self, request, slug, project_id):
- try:
- project = Project.objects.get(workspace__slug=slug, pk=project_id)
- serializer = ModuleWriteSerializer(
- data=request.data, context={"project": project}
- )
-
- if serializer.is_valid():
- serializer.save()
- return Response(serializer.data, status=status.HTTP_201_CREATED)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-
- except Project.DoesNotExist:
- return Response(
- {"error": "Project was not found"}, status=status.HTTP_404_NOT_FOUND
- )
- except IntegrityError as e:
- if "already exists" in str(e):
- return Response(
- {"name": "The module name is already taken"},
- status=status.HTTP_410_GONE,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def retrieve(self, request, slug, project_id, pk):
- try:
- queryset = self.get_queryset().get(pk=pk)
-
- assignee_distribution = (
- Issue.objects.filter(
- issue_module__module_id=pk,
- workspace__slug=slug,
- project_id=project_id,
- )
- .annotate(first_name=F("assignees__first_name"))
- .annotate(last_name=F("assignees__last_name"))
- .annotate(assignee_id=F("assignees__id"))
- .annotate(display_name=F("assignees__display_name"))
- .annotate(avatar=F("assignees__avatar"))
- .values(
- "first_name", "last_name", "assignee_id", "avatar", "display_name"
- )
- .annotate(
- total_issues=Count(
- "assignee_id",
- filter=Q(
- archived_at__isnull=True,
- is_draft=False,
- ),
- )
- )
- .annotate(
- completed_issues=Count(
- "assignee_id",
- filter=Q(
- completed_at__isnull=False,
- archived_at__isnull=True,
- is_draft=False,
- ),
- )
- )
- .annotate(
- pending_issues=Count(
- "assignee_id",
- filter=Q(
- completed_at__isnull=True,
- archived_at__isnull=True,
- is_draft=False,
- ),
- )
- )
- .order_by("first_name", "last_name")
- )
-
- label_distribution = (
- Issue.objects.filter(
- issue_module__module_id=pk,
- workspace__slug=slug,
- project_id=project_id,
- )
- .annotate(label_name=F("labels__name"))
- .annotate(color=F("labels__color"))
- .annotate(label_id=F("labels__id"))
- .values("label_name", "color", "label_id")
- .annotate(
- total_issues=Count(
- "label_id",
- filter=Q(
- archived_at__isnull=True,
- is_draft=False,
- ),
- ),
- )
- .annotate(
- completed_issues=Count(
- "label_id",
- filter=Q(
- completed_at__isnull=False,
- archived_at__isnull=True,
- is_draft=False,
- ),
- )
- )
- .annotate(
- pending_issues=Count(
- "label_id",
- filter=Q(
- completed_at__isnull=True,
- archived_at__isnull=True,
- is_draft=False,
- ),
- )
- )
- .order_by("label_name")
- )
-
- data = ModuleSerializer(queryset).data
- data["distribution"] = {
- "assignees": assignee_distribution,
- "labels": label_distribution,
- "completion_chart": {},
- }
-
- if queryset.start_date and queryset.target_date:
- data["distribution"]["completion_chart"] = burndown_plot(
- queryset=queryset, slug=slug, project_id=project_id, module_id=pk
- )
-
- return Response(
- data,
- status=status.HTTP_200_OK,
- )
-
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
+ module.delete()
+ return Response(status=status.HTTP_204_NO_CONTENT)
-class ModuleIssueViewSet(BaseViewSet):
+class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
+ """
+ This viewset automatically provides `list`, `create`, `retrieve`,
+ `update` and `destroy` actions related to module issues.
+
+ """
+
serializer_class = ModuleIssueSerializer
model = ModuleIssue
-
- filterset_fields = [
- "issue__labels__id",
- "issue__assignees__id",
- ]
+ webhook_event = "module_issue"
+ bulk = True
permission_classes = [
ProjectEntityPermission,
]
- def perform_create(self, serializer):
- serializer.save(
- project_id=self.kwargs.get("project_id"),
- module_id=self.kwargs.get("module_id"),
- )
-
- def perform_destroy(self, instance):
- issue_activity.delay(
- type="module.activity.deleted",
- requested_data=json.dumps(
- {
- "module_id": str(self.kwargs.get("module_id")),
- "issues": [str(instance.issue_id)],
- }
- ),
- actor_id=str(self.request.user.id),
- issue_id=str(self.kwargs.get("pk", None)),
- project_id=str(self.kwargs.get("project_id", None)),
- current_instance=None,
- epoch=int(timezone.now().timestamp())
- )
- return super().perform_destroy(instance)
-
def get_queryset(self):
- return self.filter_queryset(
- super()
- .get_queryset()
- .annotate(
+ return (
+ ModuleIssue.objects.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
@@ -366,253 +219,156 @@ class ModuleIssueViewSet(BaseViewSet):
.select_related("issue", "issue__state", "issue__project")
.prefetch_related("issue__assignees", "issue__labels")
.prefetch_related("module__members")
+ .order_by(self.kwargs.get("order_by", "-created_at"))
.distinct()
)
- @method_decorator(gzip_page)
- def list(self, request, slug, project_id, module_id):
- try:
- order_by = request.GET.get("order_by", "created_at")
- group_by = request.GET.get("group_by", False)
- sub_group_by = request.GET.get("sub_group_by", False)
- filters = issue_filters(request.query_params, "GET")
- issues = (
- Issue.issue_objects.filter(issue_module__module_id=module_id)
- .annotate(
- sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- .annotate(bridge_id=F("issue_module__id"))
- .filter(project_id=project_id)
- .filter(workspace__slug=slug)
- .select_related("project")
- .select_related("workspace")
- .select_related("state")
- .select_related("parent")
- .prefetch_related("assignees")
- .prefetch_related("labels")
- .order_by(order_by)
- .filter(**filters)
- .annotate(
- link_count=IssueLink.objects.filter(issue=OuterRef("id"))
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- .annotate(
- attachment_count=IssueAttachment.objects.filter(
- issue=OuterRef("id")
+ def get(self, request, slug, project_id, module_id):
+ order_by = request.GET.get("order_by", "created_at")
+ issues = (
+ Issue.issue_objects.filter(issue_module__module_id=module_id)
+ .annotate(
+ sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
+ .order_by()
+ .annotate(count=Func(F("id"), function="Count"))
+ .values("count")
+ )
+ .annotate(bridge_id=F("issue_module__id"))
+ .filter(project_id=project_id)
+ .filter(workspace__slug=slug)
+ .select_related("project")
+ .select_related("workspace")
+ .select_related("state")
+ .select_related("parent")
+ .prefetch_related("assignees")
+ .prefetch_related("labels")
+ .order_by(order_by)
+ .annotate(
+ link_count=IssueLink.objects.filter(issue=OuterRef("id"))
+ .order_by()
+ .annotate(count=Func(F("id"), function="Count"))
+ .values("count")
+ )
+ .annotate(
+ attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
+ .order_by()
+ .annotate(count=Func(F("id"), function="Count"))
+ .values("count")
+ )
+ )
+ return self.paginate(
+ request=request,
+ queryset=(issues),
+ on_results=lambda issues: IssueSerializer(
+ issues,
+ many=True,
+ fields=self.fields,
+ expand=self.expand,
+ ).data,
+ )
+
+ def post(self, request, slug, project_id, module_id):
+ issues = request.data.get("issues", [])
+ if not len(issues):
+ return Response(
+ {"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
+ )
+ module = Module.objects.get(
+ workspace__slug=slug, project_id=project_id, pk=module_id
+ )
+
+ issues = Issue.objects.filter(
+ workspace__slug=slug, project_id=project_id, pk__in=issues
+ ).values_list("id", flat=True)
+
+ module_issues = list(ModuleIssue.objects.filter(issue_id__in=issues))
+
+ update_module_issue_activity = []
+ records_to_update = []
+ record_to_create = []
+
+ for issue in issues:
+ module_issue = [
+ module_issue
+ for module_issue in module_issues
+ if str(module_issue.issue_id) in issues
+ ]
+
+ if len(module_issue):
+ if module_issue[0].module_id != module_id:
+ update_module_issue_activity.append(
+ {
+ "old_module_id": str(module_issue[0].module_id),
+ "new_module_id": str(module_id),
+ "issue_id": str(module_issue[0].issue_id),
+ }
)
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- )
-
- issues_data = IssueStateSerializer(issues, many=True).data
-
- if sub_group_by and sub_group_by == group_by:
- return Response(
- {"error": "Group by and sub group by cannot be same"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- if group_by:
- return Response(
- group_results(issues_data, group_by, sub_group_by),
- status=status.HTTP_200_OK,
- )
-
- return Response(
- issues_data,
- status=status.HTTP_200_OK,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def create(self, request, slug, project_id, module_id):
- try:
- issues = request.data.get("issues", [])
- if not len(issues):
- return Response(
- {"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
- )
- module = Module.objects.get(
- workspace__slug=slug, project_id=project_id, pk=module_id
- )
-
- module_issues = list(ModuleIssue.objects.filter(issue_id__in=issues))
-
- update_module_issue_activity = []
- records_to_update = []
- record_to_create = []
-
- for issue in issues:
- module_issue = [
- module_issue
- for module_issue in module_issues
- if str(module_issue.issue_id) in issues
- ]
-
- if len(module_issue):
- if module_issue[0].module_id != module_id:
- update_module_issue_activity.append(
- {
- "old_module_id": str(module_issue[0].module_id),
- "new_module_id": str(module_id),
- "issue_id": str(module_issue[0].issue_id),
- }
- )
- module_issue[0].module_id = module_id
- records_to_update.append(module_issue[0])
- else:
- record_to_create.append(
- ModuleIssue(
- module=module,
- issue_id=issue,
- project_id=project_id,
- workspace=module.workspace,
- created_by=request.user,
- updated_by=request.user,
- )
- )
-
- ModuleIssue.objects.bulk_create(
- record_to_create,
- batch_size=10,
- ignore_conflicts=True,
- )
-
- ModuleIssue.objects.bulk_update(
- records_to_update,
- ["module"],
- batch_size=10,
- )
-
- # Capture Issue Activity
- issue_activity.delay(
- type="module.activity.created",
- requested_data=json.dumps({"modules_list": issues}),
- actor_id=str(self.request.user.id),
- issue_id=str(self.kwargs.get("pk", None)),
- project_id=str(self.kwargs.get("project_id", None)),
- current_instance=json.dumps(
- {
- "updated_module_issues": update_module_issue_activity,
- "created_module_issues": serializers.serialize(
- "json", record_to_create
- ),
- }
- ),
- epoch=int(timezone.now().timestamp())
- )
-
- return Response(
- ModuleIssueSerializer(self.get_queryset(), many=True).data,
- status=status.HTTP_200_OK,
- )
- except Module.DoesNotExist:
- return Response(
- {"error": "Module Does not exists"}, status=status.HTTP_400_BAD_REQUEST
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class ModuleLinkViewSet(BaseViewSet):
- permission_classes = [
- ProjectEntityPermission,
- ]
-
- model = ModuleLink
- serializer_class = ModuleLinkSerializer
-
- def perform_create(self, serializer):
- serializer.save(
- project_id=self.kwargs.get("project_id"),
- module_id=self.kwargs.get("module_id"),
- )
-
- def get_queryset(self):
- return (
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
- .filter(project_id=self.kwargs.get("project_id"))
- .filter(module_id=self.kwargs.get("module_id"))
- .filter(project__project_projectmember__member=self.request.user)
- .order_by("-created_at")
- .distinct()
- )
-
-
-class ModuleFavoriteViewSet(BaseViewSet):
- serializer_class = ModuleFavoriteSerializer
- model = ModuleFavorite
-
- def get_queryset(self):
- return self.filter_queryset(
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
- .filter(user=self.request.user)
- .select_related("module")
- )
-
- def create(self, request, slug, project_id):
- try:
- serializer = ModuleFavoriteSerializer(data=request.data)
- if serializer.is_valid():
- serializer.save(user=request.user, project_id=project_id)
- return Response(serializer.data, status=status.HTTP_201_CREATED)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- except IntegrityError as e:
- if "already exists" in str(e):
- return Response(
- {"error": "The module is already added to favorites"},
- status=status.HTTP_410_GONE,
- )
+ module_issue[0].module_id = module_id
+ records_to_update.append(module_issue[0])
else:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
+ record_to_create.append(
+ ModuleIssue(
+ module=module,
+ issue_id=issue,
+ project_id=project_id,
+ workspace=module.workspace,
+ created_by=request.user,
+ updated_by=request.user,
+ )
)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- def destroy(self, request, slug, project_id, module_id):
- try:
- module_favorite = ModuleFavorite.objects.get(
- project=project_id,
- user=request.user,
- workspace__slug=slug,
- module_id=module_id,
- )
- module_favorite.delete()
- return Response(status=status.HTTP_204_NO_CONTENT)
- except ModuleFavorite.DoesNotExist:
- return Response(
- {"error": "Module is not in favorites"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
+ ModuleIssue.objects.bulk_create(
+ record_to_create,
+ batch_size=10,
+ ignore_conflicts=True,
+ )
+
+ ModuleIssue.objects.bulk_update(
+ records_to_update,
+ ["module"],
+ batch_size=10,
+ )
+
+ # Capture Issue Activity
+ issue_activity.delay(
+ type="module.activity.created",
+ requested_data=json.dumps({"modules_list": str(issues)}),
+ actor_id=str(self.request.user.id),
+ issue_id=None,
+ project_id=str(self.kwargs.get("project_id", None)),
+ current_instance=json.dumps(
+ {
+ "updated_module_issues": update_module_issue_activity,
+ "created_module_issues": serializers.serialize(
+ "json", record_to_create
+ ),
+ }
+ ),
+ epoch=int(timezone.now().timestamp()),
+ )
+
+ return Response(
+ ModuleIssueSerializer(self.get_queryset(), many=True).data,
+ status=status.HTTP_200_OK,
+ )
+
+ def delete(self, request, slug, project_id, module_id, issue_id):
+ module_issue = ModuleIssue.objects.get(
+ workspace__slug=slug, project_id=project_id, module_id=module_id, issue_id=issue_id
+ )
+ module_issue.delete()
+ issue_activity.delay(
+ type="module.activity.deleted",
+ requested_data=json.dumps(
+ {
+ "module_id": str(module_id),
+ "issues": [str(module_issue.issue_id)],
+ }
+ ),
+ actor_id=str(request.user.id),
+ issue_id=str(issue_id),
+ project_id=str(project_id),
+ current_instance=None,
+ epoch=int(timezone.now().timestamp()),
+ )
+ return Response(status=status.HTTP_204_NO_CONTENT)
\ No newline at end of file
diff --git a/apiserver/plane/api/views/notification.py b/apiserver/plane/api/views/notification.py
deleted file mode 100644
index 75b94f034b..0000000000
--- a/apiserver/plane/api/views/notification.py
+++ /dev/null
@@ -1,363 +0,0 @@
-# Django imports
-from django.db.models import Q
-from django.utils import timezone
-
-# Third party imports
-from rest_framework import status
-from rest_framework.response import Response
-from sentry_sdk import capture_exception
-from plane.utils.paginator import BasePaginator
-
-# Module imports
-from .base import BaseViewSet, BaseAPIView
-from plane.db.models import (
- Notification,
- IssueAssignee,
- IssueSubscriber,
- Issue,
- WorkspaceMember,
-)
-from plane.api.serializers import NotificationSerializer
-
-
-class NotificationViewSet(BaseViewSet, BasePaginator):
- model = Notification
- serializer_class = NotificationSerializer
-
- def get_queryset(self):
- return (
- super()
- .get_queryset()
- .filter(
- workspace__slug=self.kwargs.get("slug"),
- receiver_id=self.request.user.id,
- )
- .select_related("workspace", "project," "triggered_by", "receiver")
- )
-
- def list(self, request, slug):
- try:
- snoozed = request.GET.get("snoozed", "false")
- archived = request.GET.get("archived", "false")
- read = request.GET.get("read", "true")
-
- # Filter type
- type = request.GET.get("type", "all")
-
- notifications = (
- Notification.objects.filter(
- workspace__slug=slug, receiver_id=request.user.id
- )
- .select_related("workspace", "project", "triggered_by", "receiver")
- .order_by("snoozed_till", "-created_at")
- )
-
- # Filter for snoozed notifications
- if snoozed == "false":
- notifications = notifications.filter(
- Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
- )
-
- if snoozed == "true":
- notifications = notifications.filter(
- Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False)
- )
-
- if read == "false":
- notifications = notifications.filter(read_at__isnull=True)
-
- # Filter for archived or unarchive
- if archived == "false":
- notifications = notifications.filter(archived_at__isnull=True)
-
- if archived == "true":
- notifications = notifications.filter(archived_at__isnull=False)
-
- # Subscribed issues
- if type == "watching":
- issue_ids = IssueSubscriber.objects.filter(
- workspace__slug=slug, subscriber_id=request.user.id
- ).values_list("issue_id", flat=True)
- notifications = notifications.filter(entity_identifier__in=issue_ids)
-
- # Assigned Issues
- if type == "assigned":
- issue_ids = IssueAssignee.objects.filter(
- workspace__slug=slug, assignee_id=request.user.id
- ).values_list("issue_id", flat=True)
- notifications = notifications.filter(entity_identifier__in=issue_ids)
-
- # Created issues
- if type == "created":
- if WorkspaceMember.objects.filter(
- workspace__slug=slug, member=request.user, role__lt=15
- ).exists():
- notifications = Notification.objects.none()
- else:
- issue_ids = Issue.objects.filter(
- workspace__slug=slug, created_by=request.user
- ).values_list("pk", flat=True)
- notifications = notifications.filter(
- entity_identifier__in=issue_ids
- )
-
- # Pagination
- if request.GET.get("per_page", False) and request.GET.get("cursor", False):
- return self.paginate(
- request=request,
- queryset=(notifications),
- on_results=lambda notifications: NotificationSerializer(
- notifications, many=True
- ).data,
- )
-
- serializer = NotificationSerializer(notifications, many=True)
- return Response(serializer.data, status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def partial_update(self, request, slug, pk):
- try:
- notification = Notification.objects.get(
- workspace__slug=slug, pk=pk, receiver=request.user
- )
- # Only read_at and snoozed_till can be updated
- notification_data = {
- "snoozed_till": request.data.get("snoozed_till", None),
- }
- serializer = NotificationSerializer(
- notification, data=notification_data, partial=True
- )
-
- if serializer.is_valid():
- serializer.save()
- return Response(serializer.data, status=status.HTTP_200_OK)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- except Notification.DoesNotExist:
- return Response(
- {"error": "Notification does not exists"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def mark_read(self, request, slug, pk):
- try:
- notification = Notification.objects.get(
- receiver=request.user, workspace__slug=slug, pk=pk
- )
- notification.read_at = timezone.now()
- notification.save()
- serializer = NotificationSerializer(notification)
- return Response(serializer.data, status=status.HTTP_200_OK)
- except Notification.DoesNotExist:
- return Response(
- {"error": "Notification does not exists"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def mark_unread(self, request, slug, pk):
- try:
- notification = Notification.objects.get(
- receiver=request.user, workspace__slug=slug, pk=pk
- )
- notification.read_at = None
- notification.save()
- serializer = NotificationSerializer(notification)
- return Response(serializer.data, status=status.HTTP_200_OK)
- except Notification.DoesNotExist:
- return Response(
- {"error": "Notification does not exists"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def archive(self, request, slug, pk):
- try:
- notification = Notification.objects.get(
- receiver=request.user, workspace__slug=slug, pk=pk
- )
- notification.archived_at = timezone.now()
- notification.save()
- serializer = NotificationSerializer(notification)
- return Response(serializer.data, status=status.HTTP_200_OK)
- except Notification.DoesNotExist:
- return Response(
- {"error": "Notification does not exists"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def unarchive(self, request, slug, pk):
- try:
- notification = Notification.objects.get(
- receiver=request.user, workspace__slug=slug, pk=pk
- )
- notification.archived_at = None
- notification.save()
- serializer = NotificationSerializer(notification)
- return Response(serializer.data, status=status.HTTP_200_OK)
- except Notification.DoesNotExist:
- return Response(
- {"error": "Notification does not exists"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class UnreadNotificationEndpoint(BaseAPIView):
- def get(self, request, slug):
- try:
- # Watching Issues Count
- watching_issues_count = Notification.objects.filter(
- workspace__slug=slug,
- receiver_id=request.user.id,
- read_at__isnull=True,
- archived_at__isnull=True,
- entity_identifier__in=IssueSubscriber.objects.filter(
- workspace__slug=slug, subscriber_id=request.user.id
- ).values_list("issue_id", flat=True),
- ).count()
-
- # My Issues Count
- my_issues_count = Notification.objects.filter(
- workspace__slug=slug,
- receiver_id=request.user.id,
- read_at__isnull=True,
- archived_at__isnull=True,
- entity_identifier__in=IssueAssignee.objects.filter(
- workspace__slug=slug, assignee_id=request.user.id
- ).values_list("issue_id", flat=True),
- ).count()
-
- # Created Issues Count
- created_issues_count = Notification.objects.filter(
- workspace__slug=slug,
- receiver_id=request.user.id,
- read_at__isnull=True,
- archived_at__isnull=True,
- entity_identifier__in=Issue.objects.filter(
- workspace__slug=slug, created_by=request.user
- ).values_list("pk", flat=True),
- ).count()
-
- return Response(
- {
- "watching_issues": watching_issues_count,
- "my_issues": my_issues_count,
- "created_issues": created_issues_count,
- },
- status=status.HTTP_200_OK,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class MarkAllReadNotificationViewSet(BaseViewSet):
- def create(self, request, slug):
- try:
- snoozed = request.data.get("snoozed", False)
- archived = request.data.get("archived", False)
- type = request.data.get("type", "all")
-
- notifications = (
- Notification.objects.filter(
- workspace__slug=slug,
- receiver_id=request.user.id,
- read_at__isnull=True,
- )
- .select_related("workspace", "project", "triggered_by", "receiver")
- .order_by("snoozed_till", "-created_at")
- )
-
- # Filter for snoozed notifications
- if snoozed:
- notifications = notifications.filter(
- Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False)
- )
- else:
- notifications = notifications.filter(
- Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
- )
-
- # Filter for archived or unarchive
- if archived:
- notifications = notifications.filter(archived_at__isnull=False)
- else:
- notifications = notifications.filter(archived_at__isnull=True)
-
- # Subscribed issues
- if type == "watching":
- issue_ids = IssueSubscriber.objects.filter(
- workspace__slug=slug, subscriber_id=request.user.id
- ).values_list("issue_id", flat=True)
- notifications = notifications.filter(entity_identifier__in=issue_ids)
-
- # Assigned Issues
- if type == "assigned":
- issue_ids = IssueAssignee.objects.filter(
- workspace__slug=slug, assignee_id=request.user.id
- ).values_list("issue_id", flat=True)
- notifications = notifications.filter(entity_identifier__in=issue_ids)
-
- # Created issues
- if type == "created":
- if WorkspaceMember.objects.filter(
- workspace__slug=slug, member=request.user, role__lt=15
- ).exists():
- notifications = Notification.objects.none()
- else:
- issue_ids = Issue.objects.filter(
- workspace__slug=slug, created_by=request.user
- ).values_list("pk", flat=True)
- notifications = notifications.filter(
- entity_identifier__in=issue_ids
- )
-
- updated_notifications = []
- for notification in notifications:
- notification.read_at = timezone.now()
- updated_notifications.append(notification)
- Notification.objects.bulk_update(
- updated_notifications, ["read_at"], batch_size=100
- )
- return Response({"message": "Successful"}, status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
diff --git a/apiserver/plane/api/views/oauth.py b/apiserver/plane/api/views/oauth.py
deleted file mode 100644
index 184cba9517..0000000000
--- a/apiserver/plane/api/views/oauth.py
+++ /dev/null
@@ -1,314 +0,0 @@
-# Python imports
-import uuid
-import requests
-import os
-
-# Django imports
-from django.utils import timezone
-from django.conf import settings
-
-# Third Party modules
-from rest_framework.response import Response
-from rest_framework import exceptions
-from rest_framework.permissions import AllowAny
-from rest_framework.views import APIView
-from rest_framework_simplejwt.tokens import RefreshToken
-from rest_framework import status
-from sentry_sdk import capture_exception
-# sso authentication
-from google.oauth2 import id_token
-from google.auth.transport import requests as google_auth_request
-
-# Module imports
-from plane.db.models import SocialLoginConnection, User
-from plane.api.serializers import UserSerializer
-from .base import BaseAPIView
-
-
-def get_tokens_for_user(user):
- refresh = RefreshToken.for_user(user)
- return (
- str(refresh.access_token),
- str(refresh),
- )
-
-
-def validate_google_token(token, client_id):
- try:
- id_info = id_token.verify_oauth2_token(
- token, google_auth_request.Request(), client_id
- )
- email = id_info.get("email")
- first_name = id_info.get("given_name")
- last_name = id_info.get("family_name", "")
- data = {
- "email": email,
- "first_name": first_name,
- "last_name": last_name,
- }
- return data
- except Exception as e:
- capture_exception(e)
- raise exceptions.AuthenticationFailed("Error with Google connection.")
-
-
-def get_access_token(request_token: str, client_id: str) -> str:
- """Obtain the request token from github.
- Given the client id, client secret and request issued out by GitHub, this method
- should give back an access token
- Parameters
- ----------
- CLIENT_ID: str
- A string representing the client id issued out by github
- CLIENT_SECRET: str
- A string representing the client secret issued out by github
- request_token: str
- A string representing the request token issued out by github
- Throws
- ------
- ValueError:
- if CLIENT_ID or CLIENT_SECRET or request_token is empty or not a string
- Returns
- -------
- access_token: str
- A string representing the access token issued out by github
- """
-
- if not request_token:
- raise ValueError("The request token has to be supplied!")
-
- CLIENT_SECRET = os.environ.get("GITHUB_CLIENT_SECRET")
-
- url = f"https://github.com/login/oauth/access_token?client_id={client_id}&client_secret={CLIENT_SECRET}&code={request_token}"
- headers = {"accept": "application/json"}
-
- res = requests.post(url, headers=headers)
-
- data = res.json()
- access_token = data["access_token"]
-
- return access_token
-
-
-def get_user_data(access_token: str) -> dict:
- """
- Obtain the user data from github.
- Given the access token, this method should give back the user data
- """
- if not access_token:
- raise ValueError("The request token has to be supplied!")
- if not isinstance(access_token, str):
- raise ValueError("The request token has to be a string!")
-
- access_token = "token " + access_token
- url = "https://api.github.com/user"
- headers = {"Authorization": access_token}
-
- resp = requests.get(url=url, headers=headers)
-
- user_data = resp.json()
-
- response = requests.get(
- url="https://api.github.com/user/emails", headers=headers
- ).json()
-
- [
- user_data.update({"email": item.get("email")})
- for item in response
- if item.get("primary") is True
- ]
-
- return user_data
-
-
-class OauthEndpoint(BaseAPIView):
- permission_classes = [AllowAny]
-
- def post(self, request):
- try:
- medium = request.data.get("medium", False)
- id_token = request.data.get("credential", False)
- client_id = request.data.get("clientId", False)
-
- if not medium or not id_token:
- return Response(
- {
- "error": "Something went wrong. Please try again later or contact the support team."
- },
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- if medium == "google":
- data = validate_google_token(id_token, client_id)
-
- if medium == "github":
- access_token = get_access_token(id_token, client_id)
- data = get_user_data(access_token)
-
- email = data.get("email", None)
- if email == None:
- return Response(
- {
- "error": "Something went wrong. Please try again later or contact the support team."
- },
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- if "@" in email:
- user = User.objects.get(email=email)
- email = data["email"]
- channel = "email"
- mobile_number = uuid.uuid4().hex
- email_verified = True
- else:
- return Response(
- {
- "error": "Something went wrong. Please try again later or contact the support team."
- },
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- ## Login Case
-
- if not user.is_active:
- return Response(
- {
- "error": "Your account has been deactivated. Please contact your site administrator."
- },
- status=status.HTTP_403_FORBIDDEN,
- )
-
- user.last_active = timezone.now()
- user.last_login_time = timezone.now()
- user.last_login_ip = request.META.get("REMOTE_ADDR")
- user.last_login_medium = f"oauth"
- user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
- user.is_email_verified = email_verified
- user.save()
-
- serialized_user = UserSerializer(user).data
-
- access_token, refresh_token = get_tokens_for_user(user)
-
- data = {
- "access_token": access_token,
- "refresh_token": refresh_token,
- "user": serialized_user,
- }
-
- SocialLoginConnection.objects.update_or_create(
- medium=medium,
- extra_data={},
- user=user,
- defaults={
- "token_data": {"id_token": id_token},
- "last_login_at": timezone.now(),
- },
- )
- if settings.ANALYTICS_BASE_API:
- _ = requests.post(
- settings.ANALYTICS_BASE_API,
- headers={
- "Content-Type": "application/json",
- "X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
- },
- json={
- "event_id": uuid.uuid4().hex,
- "event_data": {
- "medium": f"oauth-{medium}",
- },
- "user": {"email": email, "id": str(user.id)},
- "device_ctx": {
- "ip": request.META.get("REMOTE_ADDR"),
- "user_agent": request.META.get("HTTP_USER_AGENT"),
- },
- "event_type": "SIGN_IN",
- },
- )
- return Response(data, status=status.HTTP_200_OK)
-
- except User.DoesNotExist:
- ## Signup Case
-
- username = uuid.uuid4().hex
-
- if "@" in email:
- email = data["email"]
- mobile_number = uuid.uuid4().hex
- channel = "email"
- email_verified = True
- else:
- return Response(
- {
- "error": "Something went wrong. Please try again later or contact the support team."
- },
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- user = User(
- username=username,
- email=email,
- mobile_number=mobile_number,
- first_name=data.get("first_name", ""),
- last_name=data.get("last_name", ""),
- is_email_verified=email_verified,
- is_password_autoset=True,
- )
-
- user.set_password(uuid.uuid4().hex)
- user.is_password_autoset = True
- user.last_active = timezone.now()
- user.last_login_time = timezone.now()
- user.last_login_ip = request.META.get("REMOTE_ADDR")
- user.last_login_medium = "oauth"
- user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
- user.token_updated_at = timezone.now()
- user.save()
- serialized_user = UserSerializer(user).data
-
- access_token, refresh_token = get_tokens_for_user(user)
- data = {
- "access_token": access_token,
- "refresh_token": refresh_token,
- "user": serialized_user,
- "permissions": [],
- }
- if settings.ANALYTICS_BASE_API:
- _ = requests.post(
- settings.ANALYTICS_BASE_API,
- headers={
- "Content-Type": "application/json",
- "X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
- },
- json={
- "event_id": uuid.uuid4().hex,
- "event_data": {
- "medium": f"oauth-{medium}",
- },
- "user": {"email": email, "id": str(user.id)},
- "device_ctx": {
- "ip": request.META.get("REMOTE_ADDR"),
- "user_agent": request.META.get("HTTP_USER_AGENT"),
- },
- "event_type": "SIGN_UP",
- },
- )
-
- SocialLoginConnection.objects.update_or_create(
- medium=medium,
- extra_data={},
- user=user,
- defaults={
- "token_data": {"id_token": id_token},
- "last_login_at": timezone.now(),
- },
- )
- return Response(data, status=status.HTTP_201_CREATED)
- except Exception as e:
- capture_exception(e)
- return Response(
- {
- "error": "Something went wrong. Please try again later or contact the support team."
- },
- status=status.HTTP_400_BAD_REQUEST,
- )
diff --git a/apiserver/plane/api/views/page.py b/apiserver/plane/api/views/page.py
deleted file mode 100644
index d9fad9eaa9..0000000000
--- a/apiserver/plane/api/views/page.py
+++ /dev/null
@@ -1,321 +0,0 @@
-# Python imports
-from datetime import timedelta, datetime, date
-
-# Django imports
-from django.db import IntegrityError
-from django.db.models import Exists, OuterRef, Q, Prefetch
-from django.utils import timezone
-
-# Third party imports
-from rest_framework import status
-from rest_framework.response import Response
-from sentry_sdk import capture_exception
-
-# Module imports
-from .base import BaseViewSet, BaseAPIView
-from plane.api.permissions import ProjectEntityPermission
-from plane.db.models import (
- Page,
- PageBlock,
- PageFavorite,
- Issue,
- IssueAssignee,
- IssueActivity,
-)
-from plane.api.serializers import (
- PageSerializer,
- PageBlockSerializer,
- PageFavoriteSerializer,
- IssueLiteSerializer,
-)
-
-
-class PageViewSet(BaseViewSet):
- serializer_class = PageSerializer
- model = Page
- permission_classes = [
- ProjectEntityPermission,
- ]
- search_fields = [
- "name",
- ]
-
- def get_queryset(self):
- subquery = PageFavorite.objects.filter(
- user=self.request.user,
- page_id=OuterRef("pk"),
- project_id=self.kwargs.get("project_id"),
- workspace__slug=self.kwargs.get("slug"),
- )
- return self.filter_queryset(
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
- .filter(project_id=self.kwargs.get("project_id"))
- .filter(project__project_projectmember__member=self.request.user)
- .filter(Q(owned_by=self.request.user) | Q(access=0))
- .select_related("project")
- .select_related("workspace")
- .select_related("owned_by")
- .annotate(is_favorite=Exists(subquery))
- .order_by(self.request.GET.get("order_by", "-created_at"))
- .prefetch_related("labels")
- .order_by("name", "-is_favorite")
- .prefetch_related(
- Prefetch(
- "blocks",
- queryset=PageBlock.objects.select_related(
- "page", "issue", "workspace", "project"
- ),
- )
- )
- .distinct()
- )
-
- def perform_create(self, serializer):
- serializer.save(
- project_id=self.kwargs.get("project_id"), owned_by=self.request.user
- )
-
- def create(self, request, slug, project_id):
- try:
- serializer = PageSerializer(
- data=request.data,
- context={"project_id": project_id, "owned_by_id": request.user.id},
- )
-
- if serializer.is_valid():
- serializer.save()
- return Response(serializer.data, status=status.HTTP_201_CREATED)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def partial_update(self, request, slug, project_id, pk):
- try:
- page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
- # Only update access if the page owner is the requesting user
- if (
- page.access != request.data.get("access", page.access)
- and page.owned_by_id != request.user.id
- ):
- return Response(
- {
- "error": "Access cannot be updated since this page is owned by someone else"
- },
- status=status.HTTP_400_BAD_REQUEST,
- )
- serializer = PageSerializer(page, data=request.data, partial=True)
- if serializer.is_valid():
- serializer.save()
- return Response(serializer.data, status=status.HTTP_200_OK)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- except Page.DoesNotExist:
- return Response(
- {"error": "Page Does not exist"}, status=status.HTTP_400_BAD_REQUEST
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def list(self, request, slug, project_id):
- try:
- queryset = self.get_queryset()
- page_view = request.GET.get("page_view", False)
-
- if not page_view:
- return Response({"error": "Page View parameter is required"}, status=status.HTTP_400_BAD_REQUEST)
-
- # All Pages
- if page_view == "all":
- return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK)
-
- # Recent pages
- if page_view == "recent":
- current_time = date.today()
- day_before = current_time - timedelta(days=1)
- todays_pages = queryset.filter(updated_at__date=date.today())
- yesterdays_pages = queryset.filter(updated_at__date=day_before)
- earlier_this_week = queryset.filter( updated_at__date__range=(
- (timezone.now() - timedelta(days=7)),
- (timezone.now() - timedelta(days=2)),
- ))
- return Response(
- {
- "today": PageSerializer(todays_pages, many=True).data,
- "yesterday": PageSerializer(yesterdays_pages, many=True).data,
- "earlier_this_week": PageSerializer(earlier_this_week, many=True).data,
- },
- status=status.HTTP_200_OK,
- )
-
- # Favorite Pages
- if page_view == "favorite":
- queryset = queryset.filter(is_favorite=True)
- return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK)
-
- # My pages
- if page_view == "created_by_me":
- queryset = queryset.filter(owned_by=request.user)
- return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK)
-
- # Created by other Pages
- if page_view == "created_by_other":
- queryset = queryset.filter(~Q(owned_by=request.user), access=0)
- return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK)
-
- return Response({"error": "No matching view found"}, status=status.HTTP_400_BAD_REQUEST)
- except Exception as e:
- capture_exception(e)
- return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST)
-
-class PageBlockViewSet(BaseViewSet):
- serializer_class = PageBlockSerializer
- model = PageBlock
- permission_classes = [
- ProjectEntityPermission,
- ]
-
- def get_queryset(self):
- return self.filter_queryset(
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
- .filter(project_id=self.kwargs.get("project_id"))
- .filter(page_id=self.kwargs.get("page_id"))
- .filter(project__project_projectmember__member=self.request.user)
- .select_related("project")
- .select_related("workspace")
- .select_related("page")
- .select_related("issue")
- .order_by("sort_order")
- .distinct()
- )
-
- def perform_create(self, serializer):
- serializer.save(
- project_id=self.kwargs.get("project_id"),
- page_id=self.kwargs.get("page_id"),
- )
-
-
-class PageFavoriteViewSet(BaseViewSet):
- permission_classes = [
- ProjectEntityPermission,
- ]
-
- serializer_class = PageFavoriteSerializer
- model = PageFavorite
-
- def get_queryset(self):
- return self.filter_queryset(
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
- .filter(user=self.request.user)
- .select_related("page", "page__owned_by")
- )
-
- def create(self, request, slug, project_id):
- try:
- serializer = PageFavoriteSerializer(data=request.data)
- if serializer.is_valid():
- serializer.save(user=request.user, project_id=project_id)
- return Response(serializer.data, status=status.HTTP_201_CREATED)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- except IntegrityError as e:
- if "already exists" in str(e):
- return Response(
- {"error": "The page is already added to favorites"},
- status=status.HTTP_410_GONE,
- )
- else:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def destroy(self, request, slug, project_id, page_id):
- try:
- page_favorite = PageFavorite.objects.get(
- project=project_id,
- user=request.user,
- workspace__slug=slug,
- page_id=page_id,
- )
- page_favorite.delete()
- return Response(status=status.HTTP_204_NO_CONTENT)
- except PageFavorite.DoesNotExist:
- return Response(
- {"error": "Page is not in favorites"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class CreateIssueFromPageBlockEndpoint(BaseAPIView):
- permission_classes = [
- ProjectEntityPermission,
- ]
-
- def post(self, request, slug, project_id, page_id, page_block_id):
- try:
- page_block = PageBlock.objects.get(
- pk=page_block_id,
- workspace__slug=slug,
- project_id=project_id,
- page_id=page_id,
- )
- issue = Issue.objects.create(
- name=page_block.name,
- project_id=project_id,
- description=page_block.description,
- description_html=page_block.description_html,
- description_stripped=page_block.description_stripped,
- )
- _ = IssueAssignee.objects.create(
- issue=issue, assignee=request.user, project_id=project_id
- )
-
- _ = IssueActivity.objects.create(
- issue=issue,
- actor=request.user,
- project_id=project_id,
- comment=f"created the issue from {page_block.name} block",
- verb="created",
- )
-
- page_block.issue = issue
- page_block.save()
-
- return Response(IssueLiteSerializer(issue).data, status=status.HTTP_200_OK)
- except PageBlock.DoesNotExist:
- return Response(
- {"error": "Page Block does not exist"}, status=status.HTTP_404_NOT_FOUND
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py
index 1ba2271778..e8dc9f5a96 100644
--- a/apiserver/plane/api/views/project.py
+++ b/apiserver/plane/api/views/project.py
@@ -1,118 +1,63 @@
-# Python imports
-import jwt
-import boto3
-from datetime import datetime
-
# Django imports
-from django.core.exceptions import ValidationError
from django.db import IntegrityError
-from django.db.models import (
- Q,
- Exists,
- OuterRef,
- Func,
- F,
- Func,
- Subquery,
-)
-from django.core.validators import validate_email
-from django.conf import settings
+from django.db.models import Exists, OuterRef, Q, F, Func, Subquery, Prefetch
-# Third Party imports
-from rest_framework.response import Response
+# Third party imports
from rest_framework import status
-from rest_framework import serializers
-from rest_framework.permissions import AllowAny
-from sentry_sdk import capture_exception
+from rest_framework.response import Response
+from rest_framework.serializers import ValidationError
# Module imports
-from .base import BaseViewSet, BaseAPIView
-from plane.api.serializers import (
- ProjectSerializer,
- ProjectMemberSerializer,
- ProjectDetailSerializer,
- ProjectMemberInviteSerializer,
- ProjectFavoriteSerializer,
- IssueLiteSerializer,
- ProjectDeployBoardSerializer,
- ProjectMemberAdminSerializer,
-)
-
-from plane.api.permissions import (
- ProjectBasePermission,
- ProjectEntityPermission,
- ProjectMemberPermission,
- ProjectLitePermission,
-)
-
from plane.db.models import (
- Project,
- ProjectMember,
Workspace,
- ProjectMemberInvite,
- User,
- WorkspaceMember,
- State,
- TeamMember,
+ Project,
ProjectFavorite,
- ProjectIdentifier,
- Module,
- Cycle,
- CycleFavorite,
- ModuleFavorite,
- PageFavorite,
- IssueViewFavorite,
- Page,
- IssueAssignee,
- ModuleMember,
- Inbox,
+ ProjectMember,
ProjectDeployBoard,
+ State,
+ Cycle,
+ Module,
+ IssueProperty,
+ Inbox,
)
-
-from plane.bgtasks.project_invitation_task import project_invitation
+from plane.app.permissions import ProjectBasePermission
+from plane.api.serializers import ProjectSerializer
+from .base import BaseAPIView, WebhookMixin
-class ProjectViewSet(BaseViewSet):
+class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
+ """Project Endpoints to create, update, list, retrieve and delete endpoint"""
+
serializer_class = ProjectSerializer
model = Project
+ webhook_event = "project"
permission_classes = [
ProjectBasePermission,
]
- def get_serializer_class(self, *args, **kwargs):
- if self.action == "update" or self.action == "partial_update":
- return ProjectSerializer
- return ProjectDetailSerializer
-
def get_queryset(self):
- subquery = ProjectFavorite.objects.filter(
- user=self.request.user,
- project_id=OuterRef("pk"),
- workspace__slug=self.kwargs.get("slug"),
- )
-
- return self.filter_queryset(
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
+ return (
+ Project.objects.filter(workspace__slug=self.kwargs.get("slug"))
.filter(Q(project_projectmember__member=self.request.user) | Q(network=2))
.select_related(
"workspace", "workspace__owner", "default_assignee", "project_lead"
)
- .annotate(is_favorite=Exists(subquery))
.annotate(
is_member=Exists(
ProjectMember.objects.filter(
member=self.request.user,
project_id=OuterRef("pk"),
workspace__slug=self.kwargs.get("slug"),
+ is_active=True,
)
)
)
.annotate(
total_members=ProjectMember.objects.filter(
- project_id=OuterRef("id"), member__is_bot=False
+ project_id=OuterRef("id"),
+ member__is_bot=False,
+ is_active=True,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
@@ -134,6 +79,7 @@ class ProjectViewSet(BaseViewSet):
member_role=ProjectMember.objects.filter(
project_id=OuterRef("pk"),
member_id=self.request.user.id,
+ is_active=True,
).values("role")
)
.annotate(
@@ -144,66 +90,46 @@ class ProjectViewSet(BaseViewSet):
)
)
)
+ .order_by(self.kwargs.get("order_by", "-created_at"))
.distinct()
)
- def list(self, request, slug):
- try:
- is_favorite = request.GET.get("is_favorite", "all")
- subquery = ProjectFavorite.objects.filter(
- user=self.request.user,
- project_id=OuterRef("pk"),
- workspace__slug=self.kwargs.get("slug"),
- )
+ def get(self, request, slug, project_id=None):
+ if project_id is None:
sort_order_query = ProjectMember.objects.filter(
member=request.user,
project_id=OuterRef("pk"),
workspace__slug=self.kwargs.get("slug"),
+ is_active=True,
).values("sort_order")
projects = (
self.get_queryset()
- .annotate(is_favorite=Exists(subquery))
.annotate(sort_order=Subquery(sort_order_query))
- .order_by("sort_order", "name")
- .annotate(
- total_members=ProjectMember.objects.filter(
- project_id=OuterRef("id")
+ .prefetch_related(
+ Prefetch(
+ "project_projectmember",
+ queryset=ProjectMember.objects.filter(
+ workspace__slug=slug,
+ is_active=True,
+ ).select_related("member"),
)
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- .annotate(
- total_cycles=Cycle.objects.filter(project_id=OuterRef("id"))
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- .annotate(
- total_modules=Module.objects.filter(project_id=OuterRef("id"))
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
)
+ .order_by(request.GET.get("order_by", "sort_order"))
)
-
- if is_favorite == "true":
- projects = projects.filter(is_favorite=True)
- if is_favorite == "false":
- projects = projects.filter(is_favorite=False)
-
- return Response(ProjectDetailSerializer(projects, many=True).data)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
+ return self.paginate(
+ request=request,
+ queryset=(projects),
+ on_results=lambda projects: ProjectSerializer(
+ projects, many=True, fields=self.fields, expand=self.expand,
+ ).data,
)
+ project = self.get_queryset().get(workspace__slug=slug, pk=project_id)
+ serializer = ProjectSerializer(project, fields=self.fields, expand=self.expand,)
+ return Response(serializer.data, status=status.HTTP_200_OK)
- def create(self, request, slug):
+ def post(self, request, slug):
try:
workspace = Workspace.objects.get(slug=slug)
-
serializer = ProjectSerializer(
data={**request.data}, context={"workspace_id": workspace.id}
)
@@ -214,6 +140,11 @@ class ProjectViewSet(BaseViewSet):
project_member = ProjectMember.objects.create(
project_id=serializer.data["id"], member=request.user, role=20
)
+ # Also create the issue property for the user
+ _ = IssueProperty.objects.create(
+ project_id=serializer.data["id"],
+ user=request.user,
+ )
if serializer.data["project_lead"] is not None and str(
serializer.data["project_lead"]
@@ -223,6 +154,11 @@ class ProjectViewSet(BaseViewSet):
member_id=serializer.data["project_lead"],
role=20,
)
+ # Also create the issue property for the user
+ IssueProperty.objects.create(
+ project_id=serializer.data["id"],
+ user_id=serializer.data["project_lead"],
+ )
# Default states
states = [
@@ -275,12 +211,9 @@ class ProjectViewSet(BaseViewSet):
]
)
- data = serializer.data
- # Additional fields of the member
- data["sort_order"] = project_member.sort_order
- data["member_role"] = project_member.role
- data["is_member"] = True
- return Response(data, status=status.HTTP_201_CREATED)
+ project = self.get_queryset().filter(pk=serializer.data["id"]).first()
+ serializer = ProjectSerializer(project)
+ return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(
serializer.errors,
status=status.HTTP_400_BAD_REQUEST,
@@ -291,33 +224,20 @@ class ProjectViewSet(BaseViewSet):
{"name": "The project name is already taken"},
status=status.HTTP_410_GONE,
)
- else:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_410_GONE,
- )
except Workspace.DoesNotExist as e:
return Response(
{"error": "Workspace does not exist"}, status=status.HTTP_404_NOT_FOUND
)
- except serializers.ValidationError as e:
+ except ValidationError as e:
return Response(
{"identifier": "The project identifier is already taken"},
status=status.HTTP_410_GONE,
)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- def partial_update(self, request, slug, pk=None):
+ def patch(self, request, slug, project_id=None):
try:
workspace = Workspace.objects.get(slug=slug)
-
- project = Project.objects.get(pk=pk)
+ project = Project.objects.get(pk=project_id)
serializer = ProjectSerializer(
project,
@@ -338,911 +258,31 @@ class ProjectViewSet(BaseViewSet):
name="Triage",
group="backlog",
description="Default state for managing all Inbox Issues",
- project_id=pk,
+ project_id=project_id,
color="#ff7700",
)
+ project = self.get_queryset().filter(pk=serializer.data["id"]).first()
+ serializer = ProjectSerializer(project)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-
except IntegrityError as e:
if "already exists" in str(e):
return Response(
{"name": "The project name is already taken"},
status=status.HTTP_410_GONE,
)
- except Project.DoesNotExist or Workspace.DoesNotExist as e:
+ except (Project.DoesNotExist, Workspace.DoesNotExist):
return Response(
{"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND
)
- except serializers.ValidationError as e:
+ except ValidationError as e:
return Response(
{"identifier": "The project identifier is already taken"},
status=status.HTTP_410_GONE,
)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class InviteProjectEndpoint(BaseAPIView):
- permission_classes = [
- ProjectBasePermission,
- ]
-
- def post(self, request, slug, project_id):
- try:
- email = request.data.get("email", False)
- role = request.data.get("role", False)
-
- # Check if email is provided
- if not email:
- return Response(
- {"error": "Email is required"}, status=status.HTTP_400_BAD_REQUEST
- )
-
- validate_email(email)
- # Check if user is already a member of workspace
- if ProjectMember.objects.filter(
- project_id=project_id,
- member__email=email,
- member__is_bot=False,
- ).exists():
- return Response(
- {"error": "User is already member of workspace"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- user = User.objects.filter(email=email).first()
-
- if user is None:
- token = jwt.encode(
- {"email": email, "timestamp": datetime.now().timestamp()},
- settings.SECRET_KEY,
- algorithm="HS256",
- )
- project_invitation_obj = ProjectMemberInvite.objects.create(
- email=email.strip().lower(),
- project_id=project_id,
- token=token,
- role=role,
- )
- domain = settings.WEB_URL
- project_invitation.delay(email, project_id, token, domain)
-
- return Response(
- {
- "message": "Email sent successfully",
- "id": project_invitation_obj.id,
- },
- status=status.HTTP_200_OK,
- )
-
- project_member = ProjectMember.objects.create(
- member=user, project_id=project_id, role=role
- )
-
- return Response(
- ProjectMemberSerializer(project_member).data, status=status.HTTP_200_OK
- )
-
- except ValidationError:
- return Response(
- {
- "error": "Invalid email address provided a valid email address is required to send the invite"
- },
- status=status.HTTP_400_BAD_REQUEST,
- )
- except (Workspace.DoesNotExist, Project.DoesNotExist) as e:
- return Response(
- {"error": "Workspace or Project does not exists"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class UserProjectInvitationsViewset(BaseViewSet):
- serializer_class = ProjectMemberInviteSerializer
- model = ProjectMemberInvite
-
- def get_queryset(self):
- return self.filter_queryset(
- super()
- .get_queryset()
- .filter(email=self.request.user.email)
- .select_related("workspace", "workspace__owner", "project")
- )
-
- def create(self, request):
- try:
- invitations = request.data.get("invitations")
- project_invitations = ProjectMemberInvite.objects.filter(
- pk__in=invitations, accepted=True
- )
- ProjectMember.objects.bulk_create(
- [
- ProjectMember(
- project=invitation.project,
- workspace=invitation.project.workspace,
- member=request.user,
- role=invitation.role,
- created_by=request.user,
- )
- for invitation in project_invitations
- ]
- )
-
- # Delete joined project invites
- project_invitations.delete()
-
- return Response(status=status.HTTP_204_NO_CONTENT)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class ProjectMemberViewSet(BaseViewSet):
- serializer_class = ProjectMemberAdminSerializer
- model = ProjectMember
- permission_classes = [
- ProjectMemberPermission,
- ]
-
- search_fields = [
- "member__display_name",
- "member__first_name",
- ]
-
- def get_queryset(self):
- return self.filter_queryset(
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
- .filter(project_id=self.kwargs.get("project_id"))
- .filter(member__is_bot=False)
- .select_related("project")
- .select_related("member")
- .select_related("workspace", "workspace__owner")
- )
-
- def partial_update(self, request, slug, project_id, pk):
- try:
- project_member = ProjectMember.objects.get(
- pk=pk, workspace__slug=slug, project_id=project_id
- )
- if request.user.id == project_member.member_id:
- return Response(
- {"error": "You cannot update your own role"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- # Check while updating user roles
- requested_project_member = ProjectMember.objects.get(
- project_id=project_id, workspace__slug=slug, member=request.user
- )
- if (
- "role" in request.data
- and int(request.data.get("role", project_member.role))
- > requested_project_member.role
- ):
- return Response(
- {
- "error": "You cannot update a role that is higher than your own role"
- },
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- serializer = ProjectMemberSerializer(
- project_member, data=request.data, partial=True
- )
-
- if serializer.is_valid():
- serializer.save()
- return Response(serializer.data, status=status.HTTP_200_OK)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- except ProjectMember.DoesNotExist:
- return Response(
- {"error": "Project Member does not exist"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def destroy(self, request, slug, project_id, pk):
- try:
- project_member = ProjectMember.objects.get(
- workspace__slug=slug, project_id=project_id, pk=pk
- )
- # check requesting user role
- requesting_project_member = ProjectMember.objects.get(
- workspace__slug=slug, member=request.user, project_id=project_id
- )
- if requesting_project_member.role < project_member.role:
- return Response(
- {
- "error": "You cannot remove a user having role higher than yourself"
- },
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- # Remove all favorites
- ProjectFavorite.objects.filter(
- workspace__slug=slug, project_id=project_id, user=project_member.member
- ).delete()
- CycleFavorite.objects.filter(
- workspace__slug=slug, project_id=project_id, user=project_member.member
- ).delete()
- ModuleFavorite.objects.filter(
- workspace__slug=slug, project_id=project_id, user=project_member.member
- ).delete()
- PageFavorite.objects.filter(
- workspace__slug=slug, project_id=project_id, user=project_member.member
- ).delete()
- IssueViewFavorite.objects.filter(
- workspace__slug=slug, project_id=project_id, user=project_member.member
- ).delete()
- # Also remove issue from issue assigned
- IssueAssignee.objects.filter(
- workspace__slug=slug,
- project_id=project_id,
- assignee=project_member.member,
- ).delete()
-
- # Remove if module member
- ModuleMember.objects.filter(
- workspace__slug=slug,
- project_id=project_id,
- member=project_member.member,
- ).delete()
- # Delete owned Pages
- Page.objects.filter(
- workspace__slug=slug,
- project_id=project_id,
- owned_by=project_member.member,
- ).delete()
- project_member.delete()
- return Response(status=status.HTTP_204_NO_CONTENT)
- except ProjectMember.DoesNotExist:
- return Response(
- {"error": "Project Member does not exist"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response({"error": "Something went wrong please try again later"})
-
-
-class AddMemberToProjectEndpoint(BaseAPIView):
- permission_classes = [
- ProjectBasePermission,
- ]
-
- def post(self, request, slug, project_id):
- try:
- members = request.data.get("members", [])
-
- # get the project
- project = Project.objects.get(pk=project_id, workspace__slug=slug)
-
- if not len(members):
- return Response(
- {"error": "Atleast one member is required"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- bulk_project_members = []
-
- project_members = (
- ProjectMember.objects.filter(
- workspace__slug=slug,
- member_id__in=[member.get("member_id") for member in members],
- )
- .values("member_id", "sort_order")
- .order_by("sort_order")
- )
-
- for member in members:
- sort_order = [
- project_member.get("sort_order")
- for project_member in project_members
- if str(project_member.get("member_id"))
- == str(member.get("member_id"))
- ]
- bulk_project_members.append(
- ProjectMember(
- member_id=member.get("member_id"),
- role=member.get("role", 10),
- project_id=project_id,
- workspace_id=project.workspace_id,
- sort_order=sort_order[0] - 10000 if len(sort_order) else 65535,
- )
- )
-
- project_members = ProjectMember.objects.bulk_create(
- bulk_project_members,
- batch_size=10,
- ignore_conflicts=True,
- )
-
- serializer = ProjectMemberSerializer(project_members, many=True)
-
- return Response(serializer.data, status=status.HTTP_201_CREATED)
- except KeyError:
- return Response(
- {"error": "Incorrect data sent"}, status=status.HTTP_400_BAD_REQUEST
- )
- except Project.DoesNotExist:
- return Response(
- {"error": "Project does not exist"}, status=status.HTTP_400_BAD_REQUEST
- )
- except IntegrityError:
- return Response(
- {"error": "User not member of the workspace"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class AddTeamToProjectEndpoint(BaseAPIView):
- permission_classes = [
- ProjectBasePermission,
- ]
-
- def post(self, request, slug, project_id):
- try:
- team_members = TeamMember.objects.filter(
- workspace__slug=slug, team__in=request.data.get("teams", [])
- ).values_list("member", flat=True)
-
- if len(team_members) == 0:
- return Response(
- {"error": "No such team exists"}, status=status.HTTP_400_BAD_REQUEST
- )
-
- workspace = Workspace.objects.get(slug=slug)
-
- project_members = []
- for member in team_members:
- project_members.append(
- ProjectMember(
- project_id=project_id,
- member_id=member,
- workspace=workspace,
- created_by=request.user,
- )
- )
-
- ProjectMember.objects.bulk_create(
- project_members, batch_size=10, ignore_conflicts=True
- )
-
- serializer = ProjectMemberSerializer(project_members, many=True)
- return Response(serializer.data, status=status.HTTP_201_CREATED)
- except IntegrityError as e:
- if "already exists" in str(e):
- return Response(
- {"error": "The team with the name already exists"},
- status=status.HTTP_410_GONE,
- )
- except Workspace.DoesNotExist:
- return Response(
- {"error": "The requested workspace could not be found"},
- status=status.HTTP_404_NOT_FOUND,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class ProjectMemberInvitationsViewset(BaseViewSet):
- serializer_class = ProjectMemberInviteSerializer
- model = ProjectMemberInvite
-
- search_fields = []
-
- permission_classes = [
- ProjectBasePermission,
- ]
-
- def get_queryset(self):
- return self.filter_queryset(
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
- .filter(project_id=self.kwargs.get("project_id"))
- .select_related("project")
- .select_related("workspace", "workspace__owner")
- )
-
-
-class ProjectMemberInviteDetailViewSet(BaseViewSet):
- serializer_class = ProjectMemberInviteSerializer
- model = ProjectMemberInvite
-
- search_fields = []
-
- permission_classes = [
- ProjectBasePermission,
- ]
-
- def get_queryset(self):
- return self.filter_queryset(
- super()
- .get_queryset()
- .select_related("project")
- .select_related("workspace", "workspace__owner")
- )
-
-
-class ProjectIdentifierEndpoint(BaseAPIView):
- permission_classes = [
- ProjectBasePermission,
- ]
-
- def get(self, request, slug):
- try:
- name = request.GET.get("name", "").strip().upper()
-
- if name == "":
- return Response(
- {"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
- )
-
- exists = ProjectIdentifier.objects.filter(
- name=name, workspace__slug=slug
- ).values("id", "name", "project")
-
- return Response(
- {"exists": len(exists), "identifiers": exists},
- status=status.HTTP_200_OK,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def delete(self, request, slug):
- try:
- name = request.data.get("name", "").strip().upper()
-
- if name == "":
- return Response(
- {"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
- )
-
- if Project.objects.filter(identifier=name, workspace__slug=slug).exists():
- return Response(
- {"error": "Cannot delete an identifier of an existing project"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- ProjectIdentifier.objects.filter(name=name, workspace__slug=slug).delete()
-
- return Response(
- status=status.HTTP_204_NO_CONTENT,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class ProjectJoinEndpoint(BaseAPIView):
- def post(self, request, slug):
- try:
- project_ids = request.data.get("project_ids", [])
-
- # Get the workspace user role
- workspace_member = WorkspaceMember.objects.get(
- member=request.user, workspace__slug=slug
- )
-
- workspace_role = workspace_member.role
- workspace = workspace_member.workspace
-
- ProjectMember.objects.bulk_create(
- [
- ProjectMember(
- project_id=project_id,
- member=request.user,
- role=20
- if workspace_role >= 15
- else (15 if workspace_role == 10 else workspace_role),
- workspace=workspace,
- created_by=request.user,
- )
- for project_id in project_ids
- ],
- ignore_conflicts=True,
- )
-
- return Response(
- {"message": "Projects joined successfully"},
- status=status.HTTP_201_CREATED,
- )
- except WorkspaceMember.DoesNotExist:
- return Response(
- {"error": "User is not a member of workspace"},
- status=status.HTTP_403_FORBIDDEN,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class ProjectUserViewsEndpoint(BaseAPIView):
- def post(self, request, slug, project_id):
- try:
- project = Project.objects.get(pk=project_id, workspace__slug=slug)
-
- project_member = ProjectMember.objects.filter(
- member=request.user, project=project
- ).first()
-
- if project_member is None:
- return Response(
- {"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN
- )
-
- view_props = project_member.view_props
- default_props = project_member.default_props
- preferences = project_member.preferences
- sort_order = project_member.sort_order
-
- project_member.view_props = request.data.get("view_props", view_props)
- project_member.default_props = request.data.get(
- "default_props", default_props
- )
- project_member.preferences = request.data.get("preferences", preferences)
- project_member.sort_order = request.data.get("sort_order", sort_order)
-
- project_member.save()
-
- return Response(status=status.HTTP_204_NO_CONTENT)
- except Project.DoesNotExist:
- return Response(
- {"error": "The requested resource does not exists"},
- status=status.HTTP_404_NOT_FOUND,
- )
- except Exception as e:
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class ProjectMemberUserEndpoint(BaseAPIView):
- def get(self, request, slug, project_id):
- try:
- project_member = ProjectMember.objects.get(
- project_id=project_id, workspace__slug=slug, member=request.user
- )
- serializer = ProjectMemberSerializer(project_member)
-
- return Response(serializer.data, status=status.HTTP_200_OK)
-
- except ProjectMember.DoesNotExist:
- return Response(
- {"error": "User not a member of the project"},
- status=status.HTTP_403_FORBIDDEN,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class ProjectFavoritesViewSet(BaseViewSet):
- serializer_class = ProjectFavoriteSerializer
- model = ProjectFavorite
-
- def get_queryset(self):
- return self.filter_queryset(
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
- .filter(user=self.request.user)
- .select_related(
- "project", "project__project_lead", "project__default_assignee"
- )
- .select_related("workspace", "workspace__owner")
- )
-
- def perform_create(self, serializer):
- serializer.save(user=self.request.user)
-
- def create(self, request, slug):
- try:
- serializer = ProjectFavoriteSerializer(data=request.data)
- if serializer.is_valid():
- serializer.save(user=request.user)
- return Response(serializer.data, status=status.HTTP_201_CREATED)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- except IntegrityError as e:
- print(str(e))
- if "already exists" in str(e):
- return Response(
- {"error": "The project is already added to favorites"},
- status=status.HTTP_410_GONE,
- )
- else:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_410_GONE,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def destroy(self, request, slug, project_id):
- try:
- project_favorite = ProjectFavorite.objects.get(
- project=project_id, user=request.user, workspace__slug=slug
- )
- project_favorite.delete()
- return Response(status=status.HTTP_204_NO_CONTENT)
- except ProjectFavorite.DoesNotExist:
- return Response(
- {"error": "Project is not in favorites"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class ProjectDeployBoardViewSet(BaseViewSet):
- permission_classes = [
- ProjectMemberPermission,
- ]
- serializer_class = ProjectDeployBoardSerializer
- model = ProjectDeployBoard
-
- def get_queryset(self):
- return (
- super()
- .get_queryset()
- .filter(
- workspace__slug=self.kwargs.get("slug"),
- project_id=self.kwargs.get("project_id"),
- )
- .select_related("project")
- )
-
- def create(self, request, slug, project_id):
- try:
- comments = request.data.get("comments", False)
- reactions = request.data.get("reactions", False)
- inbox = request.data.get("inbox", None)
- votes = request.data.get("votes", False)
- views = request.data.get(
- "views",
- {
- "list": True,
- "kanban": True,
- "calendar": True,
- "gantt": True,
- "spreadsheet": True,
- },
- )
-
- project_deploy_board, _ = ProjectDeployBoard.objects.get_or_create(
- anchor=f"{slug}/{project_id}",
- project_id=project_id,
- )
- project_deploy_board.comments = comments
- project_deploy_board.reactions = reactions
- project_deploy_board.inbox = inbox
- project_deploy_board.votes = votes
- project_deploy_board.views = views
-
- project_deploy_board.save()
-
- serializer = ProjectDeployBoardSerializer(project_deploy_board)
- return Response(serializer.data, status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class ProjectMemberEndpoint(BaseAPIView):
- permission_classes = [
- ProjectEntityPermission,
- ]
-
- def get(self, request, slug, project_id):
- try:
- project_members = ProjectMember.objects.filter(
- project_id=project_id,
- workspace__slug=slug,
- member__is_bot=False,
- ).select_related("project", "member", "workspace")
- serializer = ProjectMemberSerializer(project_members, many=True)
- return Response(serializer.data, status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class ProjectDeployBoardPublicSettingsEndpoint(BaseAPIView):
- permission_classes = [
- AllowAny,
- ]
-
- def get(self, request, slug, project_id):
- try:
- project_deploy_board = ProjectDeployBoard.objects.get(
- workspace__slug=slug, project_id=project_id
- )
- serializer = ProjectDeployBoardSerializer(project_deploy_board)
- return Response(serializer.data, status=status.HTTP_200_OK)
- except ProjectDeployBoard.DoesNotExist:
- return Response(
- {"error": "Project Deploy Board does not exists"},
- status=status.HTTP_404_NOT_FOUND,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class WorkspaceProjectDeployBoardEndpoint(BaseAPIView):
- permission_classes = [
- AllowAny,
- ]
-
- def get(self, request, slug):
- try:
- projects = (
- Project.objects.filter(workspace__slug=slug)
- .annotate(
- is_public=Exists(
- ProjectDeployBoard.objects.filter(
- workspace__slug=slug, project_id=OuterRef("pk")
- )
- )
- )
- .filter(is_public=True)
- ).values(
- "id",
- "identifier",
- "name",
- "description",
- "emoji",
- "icon_prop",
- "cover_image",
- )
-
- return Response(projects, status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class LeaveProjectEndpoint(BaseAPIView):
- permission_classes = [
- ProjectLitePermission,
- ]
def delete(self, request, slug, project_id):
- try:
- project_member = ProjectMember.objects.get(
- workspace__slug=slug,
- member=request.user,
- project_id=project_id,
- )
-
- # Only Admin case
- if (
- project_member.role == 20
- and ProjectMember.objects.filter(
- workspace__slug=slug,
- role=20,
- project_id=project_id,
- ).count()
- == 1
- ):
- return Response(
- {
- "error": "You cannot leave the project since you are the only admin of the project you should delete the project"
- },
- status=status.HTTP_400_BAD_REQUEST,
- )
- # Delete the member from workspace
- project_member.delete()
- return Response(status=status.HTTP_204_NO_CONTENT)
- except ProjectMember.DoesNotExist:
- return Response(
- {"error": "Workspace member does not exists"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class ProjectPublicCoverImagesEndpoint(BaseAPIView):
- permission_classes = [
- AllowAny,
- ]
-
- def get(self, request):
- try:
- files = []
- s3 = boto3.client(
- "s3",
- aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
- aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
- )
- params = {
- "Bucket": settings.AWS_S3_BUCKET_NAME,
- "Prefix": "static/project-cover/",
- }
-
- response = s3.list_objects_v2(**params)
- # Extracting file keys from the response
- if "Contents" in response:
- for content in response["Contents"]:
- if not content["Key"].endswith(
- "/"
- ): # This line ensures we're only getting files, not "sub-folders"
- files.append(
- f"https://{settings.AWS_S3_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}"
- )
-
- return Response(files, status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
- return Response([], status=status.HTTP_200_OK)
+ project = Project.objects.get(pk=project_id, workspace__slug=slug)
+ project.delete()
+ return Response(status=status.HTTP_204_NO_CONTENT)
\ No newline at end of file
diff --git a/apiserver/plane/api/views/state.py b/apiserver/plane/api/views/state.py
index 4fe0c82601..679c129647 100644
--- a/apiserver/plane/api/views/state.py
+++ b/apiserver/plane/api/views/state.py
@@ -2,36 +2,29 @@
from itertools import groupby
# Django imports
-from django.db import IntegrityError
from django.db.models import Q
# Third party imports
from rest_framework.response import Response
from rest_framework import status
-from sentry_sdk import capture_exception
# Module imports
-from . import BaseViewSet, BaseAPIView
+from .base import BaseAPIView
from plane.api.serializers import StateSerializer
-from plane.api.permissions import ProjectEntityPermission
+from plane.app.permissions import ProjectEntityPermission
from plane.db.models import State, Issue
-class StateViewSet(BaseViewSet):
+class StateAPIEndpoint(BaseAPIView):
serializer_class = StateSerializer
model = State
permission_classes = [
ProjectEntityPermission,
]
- def perform_create(self, serializer):
- serializer.save(project_id=self.kwargs.get("project_id"))
-
def get_queryset(self):
- return self.filter_queryset(
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
+ return (
+ State.objects.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(project__project_projectmember__member=self.request.user)
.filter(~Q(name="Triage"))
@@ -40,68 +33,55 @@ class StateViewSet(BaseViewSet):
.distinct()
)
- def create(self, request, slug, project_id):
- try:
- serializer = StateSerializer(data=request.data)
- if serializer.is_valid():
- serializer.save(project_id=project_id)
- return Response(serializer.data, status=status.HTTP_200_OK)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- except IntegrityError:
+ def post(self, request, slug, project_id):
+ serializer = StateSerializer(data=request.data, context={"project_id": project_id})
+ if serializer.is_valid():
+ serializer.save(project_id=project_id)
+ return Response(serializer.data, status=status.HTTP_200_OK)
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+ def get(self, request, slug, project_id, state_id=None):
+ if state_id:
+ serializer = StateSerializer(self.get_queryset().get(pk=state_id))
+ return Response(serializer.data, status=status.HTTP_200_OK)
+ return self.paginate(
+ request=request,
+ queryset=(self.get_queryset()),
+ on_results=lambda states: StateSerializer(
+ states,
+ many=True,
+ fields=self.fields,
+ expand=self.expand,
+ ).data,
+ )
+
+ def delete(self, request, slug, project_id, state_id):
+ state = State.objects.get(
+ ~Q(name="Triage"),
+ pk=state_id,
+ project_id=project_id,
+ workspace__slug=slug,
+ )
+
+ if state.default:
+ return Response({"error": "Default state cannot be deleted"}, status=False)
+
+ # Check for any issues in the state
+ issue_exist = Issue.issue_objects.filter(state=state_id).exists()
+
+ if issue_exist:
return Response(
- {"error": "State with the name already exists"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
+ {"error": "The state is not empty, only empty states can be deleted"},
status=status.HTTP_400_BAD_REQUEST,
)
- def list(self, request, slug, project_id):
- try:
- state_dict = dict()
- states = StateSerializer(self.get_queryset(), many=True).data
+ state.delete()
+ return Response(status=status.HTTP_204_NO_CONTENT)
- for key, value in groupby(
- sorted(states, key=lambda state: state["group"]),
- lambda state: state.get("group"),
- ):
- state_dict[str(key)] = list(value)
-
- return Response(state_dict, status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def destroy(self, request, slug, project_id, pk):
- try:
- state = State.objects.get(
- ~Q(name="Triage"),
- pk=pk, project_id=project_id, workspace__slug=slug,
- )
-
- if state.default:
- return Response(
- {"error": "Default state cannot be deleted"}, status=False
- )
-
- # Check for any issues in the state
- issue_exist = Issue.issue_objects.filter(state=pk).exists()
-
- if issue_exist:
- return Response(
- {
- "error": "The state is not empty, only empty states can be deleted"
- },
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- state.delete()
- return Response(status=status.HTTP_204_NO_CONTENT)
- except State.DoesNotExist:
- return Response({"error": "State does not exists"}, status=status.HTTP_404)
+ def patch(self, request, slug, project_id, state_id=None):
+ state = State.objects.get(workspace__slug=slug, project_id=project_id, pk=state_id)
+ serializer = StateSerializer(state, data=request.data, partial=True)
+ if serializer.is_valid():
+ serializer.save()
+ return Response(serializer.data, status=status.HTTP_200_OK)
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
\ No newline at end of file
diff --git a/apiserver/plane/api/views/user.py b/apiserver/plane/api/views/user.py
deleted file mode 100644
index 68958e5041..0000000000
--- a/apiserver/plane/api/views/user.py
+++ /dev/null
@@ -1,158 +0,0 @@
-# Third party imports
-from rest_framework.response import Response
-from rest_framework import status
-
-from sentry_sdk import capture_exception
-
-# Module imports
-from plane.api.serializers import (
- UserSerializer,
- IssueActivitySerializer,
-)
-
-from plane.api.views.base import BaseViewSet, BaseAPIView
-from plane.db.models import (
- User,
- Workspace,
- WorkspaceMemberInvite,
- Issue,
- IssueActivity,
- WorkspaceMember,
-)
-from plane.utils.paginator import BasePaginator
-
-
-class UserEndpoint(BaseViewSet):
- serializer_class = UserSerializer
- model = User
-
- def get_object(self):
- return self.request.user
-
- def retrieve(self, request):
- try:
- workspace = Workspace.objects.get(
- pk=request.user.last_workspace_id, workspace_member__member=request.user
- )
- workspace_invites = WorkspaceMemberInvite.objects.filter(
- email=request.user.email
- ).count()
- assigned_issues = Issue.issue_objects.filter(
- assignees__in=[request.user]
- ).count()
-
- serialized_data = UserSerializer(request.user).data
- serialized_data["workspace"] = {
- "last_workspace_id": request.user.last_workspace_id,
- "last_workspace_slug": workspace.slug,
- "fallback_workspace_id": request.user.last_workspace_id,
- "fallback_workspace_slug": workspace.slug,
- "invites": workspace_invites,
- }
- serialized_data.setdefault("issues", {})[
- "assigned_issues"
- ] = assigned_issues
-
- return Response(
- serialized_data,
- status=status.HTTP_200_OK,
- )
- except Workspace.DoesNotExist:
- # This exception will be hit even when the `last_workspace_id` is None
-
- workspace_invites = WorkspaceMemberInvite.objects.filter(
- email=request.user.email
- ).count()
- assigned_issues = Issue.issue_objects.filter(
- assignees__in=[request.user]
- ).count()
-
- fallback_workspace = (
- Workspace.objects.filter(workspace_member__member=request.user)
- .order_by("created_at")
- .first()
- )
-
- serialized_data = UserSerializer(request.user).data
-
- serialized_data["workspace"] = {
- "last_workspace_id": None,
- "last_workspace_slug": None,
- "fallback_workspace_id": fallback_workspace.id
- if fallback_workspace is not None
- else None,
- "fallback_workspace_slug": fallback_workspace.slug
- if fallback_workspace is not None
- else None,
- "invites": workspace_invites,
- }
- serialized_data.setdefault("issues", {})[
- "assigned_issues"
- ] = assigned_issues
-
- return Response(
- serialized_data,
- status=status.HTTP_200_OK,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class UpdateUserOnBoardedEndpoint(BaseAPIView):
- def patch(self, request):
- try:
- user = User.objects.get(pk=request.user.id)
- user.is_onboarded = request.data.get("is_onboarded", False)
- user.save()
- return Response(
- {"message": "Updated successfully"}, status=status.HTTP_200_OK
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class UpdateUserTourCompletedEndpoint(BaseAPIView):
- def patch(self, request):
- try:
- user = User.objects.get(pk=request.user.id)
- user.is_tour_completed = request.data.get("is_tour_completed", False)
- user.save()
- return Response(
- {"message": "Updated successfully"}, status=status.HTTP_200_OK
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class UserActivityEndpoint(BaseAPIView, BasePaginator):
- def get(self, request, slug):
- try:
- queryset = IssueActivity.objects.filter(
- actor=request.user, workspace__slug=slug
- ).select_related("actor", "workspace", "issue", "project")
-
- return self.paginate(
- request=request,
- queryset=queryset,
- on_results=lambda issue_activities: IssueActivitySerializer(
- issue_activities, many=True
- ).data,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
diff --git a/apiserver/plane/api/views/view.py b/apiserver/plane/api/views/view.py
deleted file mode 100644
index 435f8725a8..0000000000
--- a/apiserver/plane/api/views/view.py
+++ /dev/null
@@ -1,350 +0,0 @@
-# Django imports
-from django.db.models import (
- Prefetch,
- OuterRef,
- Func,
- F,
- Case,
- Value,
- CharField,
- When,
- Exists,
- Max,
-)
-from django.utils.decorators import method_decorator
-from django.views.decorators.gzip import gzip_page
-from django.db import IntegrityError
-from django.db.models import Prefetch, OuterRef, Exists
-
-# Third party imports
-from rest_framework.response import Response
-from rest_framework import status
-from sentry_sdk import capture_exception
-
-# Module imports
-from . import BaseViewSet, BaseAPIView
-from plane.api.serializers import (
- GlobalViewSerializer,
- IssueViewSerializer,
- IssueLiteSerializer,
- IssueViewFavoriteSerializer,
-)
-from plane.api.permissions import WorkspaceEntityPermission, ProjectEntityPermission
-from plane.db.models import (
- Workspace,
- GlobalView,
- IssueView,
- Issue,
- IssueViewFavorite,
- IssueReaction,
- IssueLink,
- IssueAttachment,
-)
-from plane.utils.issue_filters import issue_filters
-from plane.utils.grouper import group_results
-
-
-class GlobalViewViewSet(BaseViewSet):
- serializer_class = GlobalViewSerializer
- model = GlobalView
- permission_classes = [
- WorkspaceEntityPermission,
- ]
-
- def perform_create(self, serializer):
- workspace = Workspace.objects.get(slug=self.kwargs.get("slug"))
- serializer.save(workspace_id=workspace.id)
-
- def get_queryset(self):
- return self.filter_queryset(
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
- .select_related("workspace")
- .order_by(self.request.GET.get("order_by", "-created_at"))
- .distinct()
- )
-
-
-class GlobalViewIssuesViewSet(BaseViewSet):
- permission_classes = [
- WorkspaceEntityPermission,
- ]
-
- def get_queryset(self):
- return (
- Issue.issue_objects.annotate(
- sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- .filter(workspace__slug=self.kwargs.get("slug"))
- .select_related("project")
- .select_related("workspace")
- .select_related("state")
- .select_related("parent")
- .prefetch_related("assignees")
- .prefetch_related("labels")
- .prefetch_related(
- Prefetch(
- "issue_reactions",
- queryset=IssueReaction.objects.select_related("actor"),
- )
- )
- )
-
-
- @method_decorator(gzip_page)
- def list(self, request, slug):
- try:
- filters = issue_filters(request.query_params, "GET")
-
- # Custom ordering for priority and state
- priority_order = ["urgent", "high", "medium", "low", "none"]
- state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
-
- order_by_param = request.GET.get("order_by", "-created_at")
-
- issue_queryset = (
- self.get_queryset()
- .filter(**filters)
- .filter(project__project_projectmember__member=self.request.user)
- .annotate(cycle_id=F("issue_cycle__cycle_id"))
- .annotate(module_id=F("issue_module__module_id"))
- .annotate(
- link_count=IssueLink.objects.filter(issue=OuterRef("id"))
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- .annotate(
- attachment_count=IssueAttachment.objects.filter(
- issue=OuterRef("id")
- )
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- )
-
- # Priority Ordering
- if order_by_param == "priority" or order_by_param == "-priority":
- priority_order = (
- priority_order
- if order_by_param == "priority"
- else priority_order[::-1]
- )
- issue_queryset = issue_queryset.annotate(
- priority_order=Case(
- *[
- When(priority=p, then=Value(i))
- for i, p in enumerate(priority_order)
- ],
- output_field=CharField(),
- )
- ).order_by("priority_order")
-
- # State Ordering
- elif order_by_param in [
- "state__name",
- "state__group",
- "-state__name",
- "-state__group",
- ]:
- state_order = (
- state_order
- if order_by_param in ["state__name", "state__group"]
- else state_order[::-1]
- )
- issue_queryset = issue_queryset.annotate(
- state_order=Case(
- *[
- When(state__group=state_group, then=Value(i))
- for i, state_group in enumerate(state_order)
- ],
- default=Value(len(state_order)),
- output_field=CharField(),
- )
- ).order_by("state_order")
- # assignee and label ordering
- elif order_by_param in [
- "labels__name",
- "-labels__name",
- "assignees__first_name",
- "-assignees__first_name",
- ]:
- issue_queryset = issue_queryset.annotate(
- max_values=Max(
- order_by_param[1::]
- if order_by_param.startswith("-")
- else order_by_param
- )
- ).order_by(
- "-max_values" if order_by_param.startswith("-") else "max_values"
- )
- else:
- issue_queryset = issue_queryset.order_by(order_by_param)
-
- issues = IssueLiteSerializer(issue_queryset, many=True).data
-
- ## Grouping the results
- group_by = request.GET.get("group_by", False)
- sub_group_by = request.GET.get("sub_group_by", False)
- if sub_group_by and sub_group_by == group_by:
- return Response(
- {"error": "Group by and sub group by cannot be same"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- if group_by:
- return Response(
- group_results(issues, group_by, sub_group_by), status=status.HTTP_200_OK
- )
-
- return Response(issues, status=status.HTTP_200_OK)
-
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class IssueViewViewSet(BaseViewSet):
- serializer_class = IssueViewSerializer
- model = IssueView
- permission_classes = [
- ProjectEntityPermission,
- ]
-
- def perform_create(self, serializer):
- serializer.save(project_id=self.kwargs.get("project_id"))
-
- def get_queryset(self):
- subquery = IssueViewFavorite.objects.filter(
- user=self.request.user,
- view_id=OuterRef("pk"),
- project_id=self.kwargs.get("project_id"),
- workspace__slug=self.kwargs.get("slug"),
- )
- return self.filter_queryset(
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
- .filter(project_id=self.kwargs.get("project_id"))
- .filter(project__project_projectmember__member=self.request.user)
- .select_related("project")
- .select_related("workspace")
- .annotate(is_favorite=Exists(subquery))
- .order_by("-is_favorite", "name")
- .distinct()
- )
-
-
-class ViewIssuesEndpoint(BaseAPIView):
- permission_classes = [
- ProjectEntityPermission,
- ]
-
- def get(self, request, slug, project_id, view_id):
- try:
- view = IssueView.objects.get(pk=view_id)
- queries = view.query
-
- filters = issue_filters(request.query_params, "GET")
-
- issues = (
- Issue.issue_objects.filter(
- **queries, project_id=project_id, workspace__slug=slug
- )
- .filter(**filters)
- .select_related("project")
- .select_related("workspace")
- .select_related("state")
- .select_related("parent")
- .prefetch_related("assignees")
- .prefetch_related("labels")
- .prefetch_related(
- Prefetch(
- "issue_reactions",
- queryset=IssueReaction.objects.select_related("actor"),
- )
- )
- )
-
- serializer = IssueLiteSerializer(issues, many=True)
- return Response(serializer.data, status=status.HTTP_200_OK)
- except IssueView.DoesNotExist:
- return Response(
- {"error": "Issue View does not exist"}, status=status.HTTP_404_NOT_FOUND
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class IssueViewFavoriteViewSet(BaseViewSet):
- serializer_class = IssueViewFavoriteSerializer
- model = IssueViewFavorite
-
- def get_queryset(self):
- return self.filter_queryset(
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
- .filter(user=self.request.user)
- .select_related("view")
- )
-
- def create(self, request, slug, project_id):
- try:
- serializer = IssueViewFavoriteSerializer(data=request.data)
- if serializer.is_valid():
- serializer.save(user=request.user, project_id=project_id)
- return Response(serializer.data, status=status.HTTP_201_CREATED)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- except IntegrityError as e:
- if "already exists" in str(e):
- return Response(
- {"error": "The view is already added to favorites"},
- status=status.HTTP_410_GONE,
- )
- else:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def destroy(self, request, slug, project_id, view_id):
- try:
- view_favourite = IssueViewFavorite.objects.get(
- project=project_id,
- user=request.user,
- workspace__slug=slug,
- view_id=view_id,
- )
- view_favourite.delete()
- return Response(status=status.HTTP_204_NO_CONTENT)
- except IssueViewFavorite.DoesNotExist:
- return Response(
- {"error": "View is not in favorites"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py
deleted file mode 100644
index 8d518b160f..0000000000
--- a/apiserver/plane/api/views/workspace.py
+++ /dev/null
@@ -1,1533 +0,0 @@
-# Python imports
-import jwt
-from datetime import date, datetime
-from dateutil.relativedelta import relativedelta
-from uuid import uuid4
-
-# Django imports
-from django.db import IntegrityError
-from django.db.models import Prefetch
-from django.conf import settings
-from django.utils import timezone
-from django.core.exceptions import ValidationError
-from django.core.validators import validate_email
-from django.contrib.sites.shortcuts import get_current_site
-from django.db.models import (
- Prefetch,
- OuterRef,
- Func,
- F,
- Q,
- Count,
- Case,
- Value,
- CharField,
- When,
- Max,
- IntegerField,
-)
-from django.db.models.functions import ExtractWeek, Cast, ExtractDay
-from django.db.models.fields import DateField
-from django.contrib.auth.hashers import make_password
-
-# Third party modules
-from rest_framework import status
-from rest_framework.response import Response
-from rest_framework.permissions import AllowAny
-from sentry_sdk import capture_exception
-
-# Module imports
-from plane.api.serializers import (
- WorkSpaceSerializer,
- WorkSpaceMemberSerializer,
- TeamSerializer,
- WorkSpaceMemberInviteSerializer,
- UserLiteSerializer,
- ProjectMemberSerializer,
- WorkspaceThemeSerializer,
- IssueActivitySerializer,
- IssueLiteSerializer,
- WorkspaceMemberAdminSerializer,
-)
-from plane.api.views.base import BaseAPIView
-from . import BaseViewSet
-from plane.db.models import (
- User,
- Workspace,
- WorkspaceMember,
- WorkspaceMemberInvite,
- Team,
- ProjectMember,
- IssueActivity,
- Issue,
- WorkspaceTheme,
- IssueAssignee,
- ProjectFavorite,
- CycleFavorite,
- ModuleMember,
- ModuleFavorite,
- PageFavorite,
- Page,
- IssueViewFavorite,
- IssueLink,
- IssueAttachment,
- IssueSubscriber,
- Project,
- Label,
- WorkspaceMember,
- CycleIssue,
- IssueReaction,
-)
-from plane.api.permissions import (
- WorkSpaceBasePermission,
- WorkSpaceAdminPermission,
- WorkspaceEntityPermission,
- WorkspaceViewerPermission,
-)
-from plane.bgtasks.workspace_invitation_task import workspace_invitation
-from plane.utils.issue_filters import issue_filters
-from plane.utils.grouper import group_results
-
-
-class WorkSpaceViewSet(BaseViewSet):
- model = Workspace
- serializer_class = WorkSpaceSerializer
- permission_classes = [
- WorkSpaceBasePermission,
- ]
-
- search_fields = [
- "name",
- ]
- filterset_fields = [
- "owner",
- ]
-
- lookup_field = "slug"
-
- def get_queryset(self):
- member_count = (
- WorkspaceMember.objects.filter(
- workspace=OuterRef("id"), member__is_bot=False
- )
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
-
- issue_count = (
- Issue.issue_objects.filter(workspace=OuterRef("id"))
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- return (
- self.filter_queryset(super().get_queryset().select_related("owner"))
- .order_by("name")
- .filter(workspace_member__member=self.request.user)
- .annotate(total_members=member_count)
- .annotate(total_issues=issue_count)
- .select_related("owner")
- )
-
- def create(self, request):
- try:
- serializer = WorkSpaceSerializer(data=request.data)
-
- slug = request.data.get("slug", False)
- name = request.data.get("name", False)
-
- if not name or not slug:
- return Response(
- {"error": "Both name and slug are required"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- if len(name) > 80 or len(slug) > 48:
- return Response(
- {"error": "The maximum length for name is 80 and for slug is 48"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- if serializer.is_valid():
- serializer.save(owner=request.user)
- # Create Workspace member
- _ = WorkspaceMember.objects.create(
- workspace_id=serializer.data["id"],
- member=request.user,
- role=20,
- company_role=request.data.get("company_role", ""),
- )
- return Response(serializer.data, status=status.HTTP_201_CREATED)
- return Response(
- [serializer.errors[error][0] for error in serializer.errors],
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- ## Handling unique integrity error for now
- ## TODO: Extend this to handle other common errors which are not automatically handled by APIException
- except IntegrityError as e:
- if "already exists" in str(e):
- return Response(
- {"slug": "The workspace with the slug already exists"},
- status=status.HTTP_410_GONE,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {
- "error": "Something went wrong please try again later",
- "identifier": None,
- },
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class UserWorkSpacesEndpoint(BaseAPIView):
- search_fields = [
- "name",
- ]
- filterset_fields = [
- "owner",
- ]
-
- def get(self, request):
- try:
- member_count = (
- WorkspaceMember.objects.filter(
- workspace=OuterRef("id"), member__is_bot=False
- )
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
-
- issue_count = (
- Issue.issue_objects.filter(workspace=OuterRef("id"))
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
-
- workspace = (
- (
- Workspace.objects.prefetch_related(
- Prefetch(
- "workspace_member", queryset=WorkspaceMember.objects.all()
- )
- )
- .filter(
- workspace_member__member=request.user,
- )
- .select_related("owner")
- )
- .annotate(total_members=member_count)
- .annotate(total_issues=issue_count)
- )
-
- serializer = WorkSpaceSerializer(self.filter_queryset(workspace), many=True)
- return Response(serializer.data, status=status.HTTP_200_OK)
-
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView):
- def get(self, request):
- try:
- slug = request.GET.get("slug", False)
-
- if not slug or slug == "":
- return Response(
- {"error": "Workspace Slug is required"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- workspace = Workspace.objects.filter(slug=slug).exists()
- return Response({"status": not workspace}, status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class InviteWorkspaceEndpoint(BaseAPIView):
- permission_classes = [
- WorkSpaceAdminPermission,
- ]
-
- def post(self, request, slug):
- try:
- emails = request.data.get("emails", False)
- # Check if email is provided
- if not emails or not len(emails):
- return Response(
- {"error": "Emails are required"}, status=status.HTTP_400_BAD_REQUEST
- )
-
- # check for role level
- requesting_user = WorkspaceMember.objects.get(
- workspace__slug=slug, member=request.user
- )
- if len(
- [
- email
- for email in emails
- if int(email.get("role", 10)) > requesting_user.role
- ]
- ):
- return Response(
- {"error": "You cannot invite a user with higher role"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- workspace = Workspace.objects.get(slug=slug)
-
- # Check if user is already a member of workspace
- workspace_members = WorkspaceMember.objects.filter(
- workspace_id=workspace.id,
- member__email__in=[email.get("email") for email in emails],
- ).select_related("member", "workspace", "workspace__owner")
-
- if len(workspace_members):
- return Response(
- {
- "error": "Some users are already member of workspace",
- "workspace_users": WorkSpaceMemberSerializer(
- workspace_members, many=True
- ).data,
- },
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- workspace_invitations = []
- for email in emails:
- try:
- validate_email(email.get("email"))
- workspace_invitations.append(
- WorkspaceMemberInvite(
- email=email.get("email").strip().lower(),
- workspace_id=workspace.id,
- token=jwt.encode(
- {
- "email": email,
- "timestamp": datetime.now().timestamp(),
- },
- settings.SECRET_KEY,
- algorithm="HS256",
- ),
- role=email.get("role", 10),
- created_by=request.user,
- )
- )
- except ValidationError:
- return Response(
- {
- "error": f"Invalid email - {email} provided a valid email address is required to send the invite"
- },
- status=status.HTTP_400_BAD_REQUEST,
- )
- WorkspaceMemberInvite.objects.bulk_create(
- workspace_invitations, batch_size=10, ignore_conflicts=True
- )
-
- workspace_invitations = WorkspaceMemberInvite.objects.filter(
- email__in=[email.get("email") for email in emails]
- ).select_related("workspace")
-
- # create the user if signup is disabled
- if settings.DOCKERIZED and not settings.ENABLE_SIGNUP:
- _ = User.objects.bulk_create(
- [
- User(
- username=str(uuid4().hex),
- email=invitation.email,
- password=make_password(uuid4().hex),
- is_password_autoset=True,
- )
- for invitation in workspace_invitations
- ],
- batch_size=100,
- )
-
- for invitation in workspace_invitations:
- workspace_invitation.delay(
- invitation.email,
- workspace.id,
- invitation.token,
- settings.WEB_URL,
- request.user.email,
- )
-
- return Response(
- {
- "message": "Emails sent successfully",
- },
- status=status.HTTP_200_OK,
- )
-
- except Workspace.DoesNotExist:
- return Response(
- {"error": "Workspace does not exists"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class JoinWorkspaceEndpoint(BaseAPIView):
- permission_classes = [
- AllowAny,
- ]
-
- def post(self, request, slug, pk):
- try:
- workspace_invite = WorkspaceMemberInvite.objects.get(
- pk=pk, workspace__slug=slug
- )
-
- email = request.data.get("email", "")
-
- if email == "" or workspace_invite.email != email:
- return Response(
- {"error": "You do not have permission to join the workspace"},
- status=status.HTTP_403_FORBIDDEN,
- )
-
- if workspace_invite.responded_at is None:
- workspace_invite.accepted = request.data.get("accepted", False)
- workspace_invite.responded_at = timezone.now()
- workspace_invite.save()
-
- if workspace_invite.accepted:
- # Check if the user created account after invitation
- user = User.objects.filter(email=email).first()
-
- # If the user is present then create the workspace member
- if user is not None:
- WorkspaceMember.objects.create(
- workspace=workspace_invite.workspace,
- member=user,
- role=workspace_invite.role,
- )
-
- user.last_workspace_id = workspace_invite.workspace.id
- user.save()
-
- # Delete the invitation
- workspace_invite.delete()
-
- return Response(
- {"message": "Workspace Invitation Accepted"},
- status=status.HTTP_200_OK,
- )
-
- return Response(
- {"message": "Workspace Invitation was not accepted"},
- status=status.HTTP_200_OK,
- )
-
- return Response(
- {"error": "You have already responded to the invitation request"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- except WorkspaceMemberInvite.DoesNotExist:
- return Response(
- {"error": "The invitation either got expired or could not be found"},
- status=status.HTTP_404_NOT_FOUND,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class WorkspaceInvitationsViewset(BaseViewSet):
- serializer_class = WorkSpaceMemberInviteSerializer
- model = WorkspaceMemberInvite
-
- permission_classes = [
- WorkSpaceAdminPermission,
- ]
-
- def get_queryset(self):
- return self.filter_queryset(
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
- .select_related("workspace", "workspace__owner", "created_by")
- )
-
- def destroy(self, request, slug, pk):
- try:
- workspace_member_invite = WorkspaceMemberInvite.objects.get(
- pk=pk, workspace__slug=slug
- )
- # delete the user if signup is disabled
- if settings.DOCKERIZED and not settings.ENABLE_SIGNUP:
- user = User.objects.filter(email=workspace_member_invite.email).first()
- if user is not None:
- user.delete()
- workspace_member_invite.delete()
- return Response(status=status.HTTP_204_NO_CONTENT)
- except WorkspaceMemberInvite.DoesNotExist:
- return Response(
- {"error": "Workspace member invite does not exists"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class UserWorkspaceInvitationsEndpoint(BaseViewSet):
- serializer_class = WorkSpaceMemberInviteSerializer
- model = WorkspaceMemberInvite
-
- def get_queryset(self):
- return self.filter_queryset(
- super()
- .get_queryset()
- .filter(email=self.request.user.email)
- .select_related("workspace", "workspace__owner", "created_by")
- .annotate(total_members=Count("workspace__workspace_member"))
- )
-
- def create(self, request):
- try:
- invitations = request.data.get("invitations")
- workspace_invitations = WorkspaceMemberInvite.objects.filter(
- pk__in=invitations
- )
-
- WorkspaceMember.objects.bulk_create(
- [
- WorkspaceMember(
- workspace=invitation.workspace,
- member=request.user,
- role=invitation.role,
- created_by=request.user,
- )
- for invitation in workspace_invitations
- ],
- ignore_conflicts=True,
- )
-
- # Delete joined workspace invites
- workspace_invitations.delete()
-
- return Response(status=status.HTTP_204_NO_CONTENT)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class WorkSpaceMemberViewSet(BaseViewSet):
- serializer_class = WorkspaceMemberAdminSerializer
- model = WorkspaceMember
-
- permission_classes = [
- WorkSpaceAdminPermission,
- ]
-
- search_fields = [
- "member__display_name",
- "member__first_name",
- ]
-
- def get_queryset(self):
- return self.filter_queryset(
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"), member__is_bot=False)
- .select_related("workspace", "workspace__owner")
- .select_related("member")
- )
-
- def partial_update(self, request, slug, pk):
- try:
- workspace_member = WorkspaceMember.objects.get(pk=pk, workspace__slug=slug)
- if request.user.id == workspace_member.member_id:
- return Response(
- {"error": "You cannot update your own role"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- # Get the requested user role
- requested_workspace_member = WorkspaceMember.objects.get(
- workspace__slug=slug, member=request.user
- )
- # Check if role is being updated
- # One cannot update role higher than his own role
- if (
- "role" in request.data
- and int(request.data.get("role", workspace_member.role))
- > requested_workspace_member.role
- ):
- return Response(
- {
- "error": "You cannot update a role that is higher than your own role"
- },
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- serializer = WorkSpaceMemberSerializer(
- workspace_member, data=request.data, partial=True
- )
-
- if serializer.is_valid():
- serializer.save()
- return Response(serializer.data, status=status.HTTP_200_OK)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- except WorkspaceMember.DoesNotExist:
- return Response(
- {"error": "Workspace Member does not exist"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- def destroy(self, request, slug, pk):
- try:
- # Check the user role who is deleting the user
- workspace_member = WorkspaceMember.objects.get(workspace__slug=slug, pk=pk)
-
- # check requesting user role
- requesting_workspace_member = WorkspaceMember.objects.get(
- workspace__slug=slug, member=request.user
- )
- if requesting_workspace_member.role < workspace_member.role:
- return Response(
- {"error": "You cannot remove a user having role higher than you"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- # Check for the only member in the workspace
- if (
- workspace_member.role == 20
- and WorkspaceMember.objects.filter(
- workspace__slug=slug,
- role=20,
- member__is_bot=False,
- ).count()
- == 1
- ):
- return Response(
- {"error": "Cannot delete the only Admin for the workspace"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- # Delete the user also from all the projects
- ProjectMember.objects.filter(
- workspace__slug=slug, member=workspace_member.member
- ).delete()
- # Remove all favorites
- ProjectFavorite.objects.filter(
- workspace__slug=slug, user=workspace_member.member
- ).delete()
- CycleFavorite.objects.filter(
- workspace__slug=slug, user=workspace_member.member
- ).delete()
- ModuleFavorite.objects.filter(
- workspace__slug=slug, user=workspace_member.member
- ).delete()
- PageFavorite.objects.filter(
- workspace__slug=slug, user=workspace_member.member
- ).delete()
- IssueViewFavorite.objects.filter(
- workspace__slug=slug, user=workspace_member.member
- ).delete()
- # Also remove issue from issue assigned
- IssueAssignee.objects.filter(
- workspace__slug=slug, assignee=workspace_member.member
- ).delete()
-
- # Remove if module member
- ModuleMember.objects.filter(
- workspace__slug=slug, member=workspace_member.member
- ).delete()
- # Delete owned Pages
- Page.objects.filter(
- workspace__slug=slug, owned_by=workspace_member.member
- ).delete()
-
- workspace_member.delete()
- return Response(status=status.HTTP_204_NO_CONTENT)
- except WorkspaceMember.DoesNotExist:
- return Response(
- {"error": "Workspace Member does not exists"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class TeamMemberViewSet(BaseViewSet):
- serializer_class = TeamSerializer
- model = Team
- permission_classes = [
- WorkSpaceAdminPermission,
- ]
-
- search_fields = [
- "member__display_name",
- "member__first_name",
- ]
-
- def get_queryset(self):
- return self.filter_queryset(
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
- .select_related("workspace", "workspace__owner")
- .prefetch_related("members")
- )
-
- def create(self, request, slug):
- try:
- members = list(
- WorkspaceMember.objects.filter(
- workspace__slug=slug, member__id__in=request.data.get("members", [])
- )
- .annotate(member_str_id=Cast("member", output_field=CharField()))
- .distinct()
- .values_list("member_str_id", flat=True)
- )
-
- if len(members) != len(request.data.get("members", [])):
- users = list(set(request.data.get("members", [])).difference(members))
- users = User.objects.filter(pk__in=users)
-
- serializer = UserLiteSerializer(users, many=True)
- return Response(
- {
- "error": f"{len(users)} of the member(s) are not a part of the workspace",
- "members": serializer.data,
- },
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- workspace = Workspace.objects.get(slug=slug)
-
- serializer = TeamSerializer(
- data=request.data, context={"workspace": workspace}
- )
- if serializer.is_valid():
- serializer.save()
- return Response(serializer.data, status=status.HTTP_201_CREATED)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- except IntegrityError as e:
- if "already exists" in str(e):
- return Response(
- {"error": "The team with the name already exists"},
- status=status.HTTP_410_GONE,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class UserWorkspaceInvitationEndpoint(BaseViewSet):
- model = WorkspaceMemberInvite
- serializer_class = WorkSpaceMemberInviteSerializer
-
- permission_classes = [
- AllowAny,
- ]
-
- def get_queryset(self):
- return self.filter_queryset(
- super()
- .get_queryset()
- .filter(pk=self.kwargs.get("pk"))
- .select_related("workspace")
- )
-
-
-class UserLastProjectWithWorkspaceEndpoint(BaseAPIView):
- def get(self, request):
- try:
- user = User.objects.get(pk=request.user.id)
-
- last_workspace_id = user.last_workspace_id
-
- if last_workspace_id is None:
- return Response(
- {
- "project_details": [],
- "workspace_details": {},
- },
- status=status.HTTP_200_OK,
- )
-
- workspace = Workspace.objects.get(pk=last_workspace_id)
- workspace_serializer = WorkSpaceSerializer(workspace)
-
- project_member = ProjectMember.objects.filter(
- workspace_id=last_workspace_id, member=request.user
- ).select_related("workspace", "project", "member", "workspace__owner")
-
- project_member_serializer = ProjectMemberSerializer(
- project_member, many=True
- )
-
- return Response(
- {
- "workspace_details": workspace_serializer.data,
- "project_details": project_member_serializer.data,
- },
- status=status.HTTP_200_OK,
- )
-
- except User.DoesNotExist:
- return Response({"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class WorkspaceMemberUserEndpoint(BaseAPIView):
- def get(self, request, slug):
- try:
- workspace_member = WorkspaceMember.objects.get(
- member=request.user, workspace__slug=slug
- )
- serializer = WorkSpaceMemberSerializer(workspace_member)
- return Response(serializer.data, status=status.HTTP_200_OK)
- except (Workspace.DoesNotExist, WorkspaceMember.DoesNotExist):
- return Response({"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class WorkspaceMemberUserViewsEndpoint(BaseAPIView):
- def post(self, request, slug):
- try:
- workspace_member = WorkspaceMember.objects.get(
- workspace__slug=slug, member=request.user
- )
- workspace_member.view_props = request.data.get("view_props", {})
- workspace_member.save()
-
- return Response(status=status.HTTP_204_NO_CONTENT)
- except WorkspaceMember.DoesNotExist:
- return Response(
- {"error": "User not a member of workspace"},
- status=status.HTTP_403_FORBIDDEN,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class UserActivityGraphEndpoint(BaseAPIView):
- def get(self, request, slug):
- try:
- issue_activities = (
- IssueActivity.objects.filter(
- actor=request.user,
- workspace__slug=slug,
- created_at__date__gte=date.today() + relativedelta(months=-6),
- )
- .annotate(created_date=Cast("created_at", DateField()))
- .values("created_date")
- .annotate(activity_count=Count("created_date"))
- .order_by("created_date")
- )
-
- return Response(issue_activities, status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class UserIssueCompletedGraphEndpoint(BaseAPIView):
- def get(self, request, slug):
- try:
- month = request.GET.get("month", 1)
-
- issues = (
- Issue.issue_objects.filter(
- assignees__in=[request.user],
- workspace__slug=slug,
- completed_at__month=month,
- completed_at__isnull=False,
- )
- .annotate(completed_week=ExtractWeek("completed_at"))
- .annotate(week=F("completed_week") % 4)
- .values("week")
- .annotate(completed_count=Count("completed_week"))
- .order_by("week")
- )
-
- return Response(issues, status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class WeekInMonth(Func):
- function = "FLOOR"
- template = "(((%(expressions)s - 1) / 7) + 1)::INTEGER"
-
-
-class UserWorkspaceDashboardEndpoint(BaseAPIView):
- def get(self, request, slug):
- try:
- issue_activities = (
- IssueActivity.objects.filter(
- actor=request.user,
- workspace__slug=slug,
- created_at__date__gte=date.today() + relativedelta(months=-3),
- )
- .annotate(created_date=Cast("created_at", DateField()))
- .values("created_date")
- .annotate(activity_count=Count("created_date"))
- .order_by("created_date")
- )
-
- month = request.GET.get("month", 1)
-
- completed_issues = (
- Issue.issue_objects.filter(
- assignees__in=[request.user],
- workspace__slug=slug,
- completed_at__month=month,
- completed_at__isnull=False,
- )
- .annotate(day_of_month=ExtractDay("completed_at"))
- .annotate(week_in_month=WeekInMonth(F("day_of_month")))
- .values("week_in_month")
- .annotate(completed_count=Count("id"))
- .order_by("week_in_month")
- )
-
- assigned_issues = Issue.issue_objects.filter(
- workspace__slug=slug, assignees__in=[request.user]
- ).count()
-
- pending_issues_count = Issue.issue_objects.filter(
- ~Q(state__group__in=["completed", "cancelled"]),
- workspace__slug=slug,
- assignees__in=[request.user],
- ).count()
-
- completed_issues_count = Issue.issue_objects.filter(
- workspace__slug=slug,
- assignees__in=[request.user],
- state__group="completed",
- ).count()
-
- issues_due_week = (
- Issue.issue_objects.filter(
- workspace__slug=slug,
- assignees__in=[request.user],
- )
- .annotate(target_week=ExtractWeek("target_date"))
- .filter(target_week=timezone.now().date().isocalendar()[1])
- .count()
- )
-
- state_distribution = (
- Issue.issue_objects.filter(
- workspace__slug=slug, assignees__in=[request.user]
- )
- .annotate(state_group=F("state__group"))
- .values("state_group")
- .annotate(state_count=Count("state_group"))
- .order_by("state_group")
- )
-
- overdue_issues = Issue.issue_objects.filter(
- ~Q(state__group__in=["completed", "cancelled"]),
- workspace__slug=slug,
- assignees__in=[request.user],
- target_date__lt=timezone.now(),
- completed_at__isnull=True,
- ).values("id", "name", "workspace__slug", "project_id", "target_date")
-
- upcoming_issues = Issue.issue_objects.filter(
- ~Q(state__group__in=["completed", "cancelled"]),
- start_date__gte=timezone.now(),
- workspace__slug=slug,
- assignees__in=[request.user],
- completed_at__isnull=True,
- ).values("id", "name", "workspace__slug", "project_id", "start_date")
-
- return Response(
- {
- "issue_activities": issue_activities,
- "completed_issues": completed_issues,
- "assigned_issues_count": assigned_issues,
- "pending_issues_count": pending_issues_count,
- "completed_issues_count": completed_issues_count,
- "issues_due_week_count": issues_due_week,
- "state_distribution": state_distribution,
- "overdue_issues": overdue_issues,
- "upcoming_issues": upcoming_issues,
- },
- status=status.HTTP_200_OK,
- )
-
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class WorkspaceThemeViewSet(BaseViewSet):
- permission_classes = [
- WorkSpaceAdminPermission,
- ]
- model = WorkspaceTheme
- serializer_class = WorkspaceThemeSerializer
-
- def get_queryset(self):
- return super().get_queryset().filter(workspace__slug=self.kwargs.get("slug"))
-
- def create(self, request, slug):
- try:
- workspace = Workspace.objects.get(slug=slug)
- serializer = WorkspaceThemeSerializer(data=request.data)
- if serializer.is_valid():
- serializer.save(workspace=workspace, actor=request.user)
- return Response(serializer.data, status=status.HTTP_201_CREATED)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- except Workspace.DoesNotExist:
- return Response(
- {"error": "Workspace does not exist"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
- def get(self, request, slug, user_id):
- try:
- filters = issue_filters(request.query_params, "GET")
-
- state_distribution = (
- Issue.issue_objects.filter(
- workspace__slug=slug,
- assignees__in=[user_id],
- project__project_projectmember__member=request.user,
- )
- .filter(**filters)
- .annotate(state_group=F("state__group"))
- .values("state_group")
- .annotate(state_count=Count("state_group"))
- .order_by("state_group")
- )
-
- priority_order = ["urgent", "high", "medium", "low", "none"]
-
- priority_distribution = (
- Issue.issue_objects.filter(
- workspace__slug=slug,
- assignees__in=[user_id],
- project__project_projectmember__member=request.user,
- )
- .filter(**filters)
- .values("priority")
- .annotate(priority_count=Count("priority"))
- .filter(priority_count__gte=1)
- .annotate(
- priority_order=Case(
- *[
- When(priority=p, then=Value(i))
- for i, p in enumerate(priority_order)
- ],
- default=Value(len(priority_order)),
- output_field=IntegerField(),
- )
- )
- .order_by("priority_order")
- )
-
- created_issues = (
- Issue.issue_objects.filter(
- workspace__slug=slug,
- project__project_projectmember__member=request.user,
- created_by_id=user_id,
- )
- .filter(**filters)
- .count()
- )
-
- assigned_issues_count = (
- Issue.issue_objects.filter(
- workspace__slug=slug,
- assignees__in=[user_id],
- project__project_projectmember__member=request.user,
- )
- .filter(**filters)
- .count()
- )
-
- pending_issues_count = (
- Issue.issue_objects.filter(
- ~Q(state__group__in=["completed", "cancelled"]),
- workspace__slug=slug,
- assignees__in=[user_id],
- project__project_projectmember__member=request.user,
- )
- .filter(**filters)
- .count()
- )
-
- completed_issues_count = (
- Issue.issue_objects.filter(
- workspace__slug=slug,
- assignees__in=[user_id],
- state__group="completed",
- project__project_projectmember__member=request.user,
- )
- .filter(**filters)
- .count()
- )
-
- subscribed_issues_count = (
- IssueSubscriber.objects.filter(
- workspace__slug=slug,
- subscriber_id=user_id,
- project__project_projectmember__member=request.user,
- )
- .filter(**filters)
- .count()
- )
-
- upcoming_cycles = CycleIssue.objects.filter(
- workspace__slug=slug,
- cycle__start_date__gt=timezone.now().date(),
- issue__assignees__in=[
- user_id,
- ],
- ).values("cycle__name", "cycle__id", "cycle__project_id")
-
- present_cycle = CycleIssue.objects.filter(
- workspace__slug=slug,
- cycle__start_date__lt=timezone.now().date(),
- cycle__end_date__gt=timezone.now().date(),
- issue__assignees__in=[
- user_id,
- ],
- ).values("cycle__name", "cycle__id", "cycle__project_id")
-
- return Response(
- {
- "state_distribution": state_distribution,
- "priority_distribution": priority_distribution,
- "created_issues": created_issues,
- "assigned_issues": assigned_issues_count,
- "completed_issues": completed_issues_count,
- "pending_issues": pending_issues_count,
- "subscribed_issues": subscribed_issues_count,
- "present_cycles": present_cycle,
- "upcoming_cycles": upcoming_cycles,
- }
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class WorkspaceUserActivityEndpoint(BaseAPIView):
- permission_classes = [
- WorkspaceEntityPermission,
- ]
-
- def get(self, request, slug, user_id):
- try:
- projects = request.query_params.getlist("project", [])
-
- queryset = IssueActivity.objects.filter(
- ~Q(field__in=["comment", "vote", "reaction", "draft"]),
- workspace__slug=slug,
- project__project_projectmember__member=request.user,
- actor=user_id,
- ).select_related("actor", "workspace", "issue", "project")
-
- if projects:
- queryset = queryset.filter(project__in=projects)
-
- return self.paginate(
- request=request,
- queryset=queryset,
- on_results=lambda issue_activities: IssueActivitySerializer(
- issue_activities, many=True
- ).data,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class WorkspaceUserProfileEndpoint(BaseAPIView):
- def get(self, request, slug, user_id):
- try:
- user_data = User.objects.get(pk=user_id)
-
- requesting_workspace_member = WorkspaceMember.objects.get(
- workspace__slug=slug, member=request.user
- )
- projects = []
- if requesting_workspace_member.role >= 10:
- projects = (
- Project.objects.filter(
- workspace__slug=slug,
- project_projectmember__member=request.user,
- )
- .annotate(
- created_issues=Count(
- "project_issue",
- filter=Q(
- project_issue__created_by_id=user_id,
- project_issue__archived_at__isnull=True,
- project_issue__is_draft=False,
- ),
- )
- )
- .annotate(
- assigned_issues=Count(
- "project_issue",
- filter=Q(
- project_issue__assignees__in=[user_id],
- project_issue__archived_at__isnull=True,
- project_issue__is_draft=False,
- ),
- )
- )
- .annotate(
- completed_issues=Count(
- "project_issue",
- filter=Q(
- project_issue__completed_at__isnull=False,
- project_issue__assignees__in=[user_id],
- project_issue__archived_at__isnull=True,
- project_issue__is_draft=False,
- ),
- )
- )
- .annotate(
- pending_issues=Count(
- "project_issue",
- filter=Q(
- project_issue__state__group__in=[
- "backlog",
- "unstarted",
- "started",
- ],
- project_issue__assignees__in=[user_id],
- project_issue__archived_at__isnull=True,
- project_issue__is_draft=False,
- ),
- )
- )
- .values(
- "id",
- "name",
- "identifier",
- "emoji",
- "icon_prop",
- "created_issues",
- "assigned_issues",
- "completed_issues",
- "pending_issues",
- )
- )
-
- return Response(
- {
- "project_data": projects,
- "user_data": {
- "email": user_data.email,
- "first_name": user_data.first_name,
- "last_name": user_data.last_name,
- "avatar": user_data.avatar,
- "cover_image": user_data.cover_image,
- "date_joined": user_data.date_joined,
- "user_timezone": user_data.user_timezone,
- "display_name": user_data.display_name,
- },
- },
- status=status.HTTP_200_OK,
- )
- except WorkspaceMember.DoesNotExist:
- return Response({"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
- permission_classes = [
- WorkspaceViewerPermission,
- ]
-
- def get(self, request, slug, user_id):
- try:
- filters = issue_filters(request.query_params, "GET")
-
- # Custom ordering for priority and state
- priority_order = ["urgent", "high", "medium", "low", "none"]
- state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
-
- order_by_param = request.GET.get("order_by", "-created_at")
- issue_queryset = (
- Issue.issue_objects.filter(
- Q(assignees__in=[user_id])
- | Q(created_by_id=user_id)
- | Q(issue_subscribers__subscriber_id=user_id),
- workspace__slug=slug,
- project__project_projectmember__member=request.user,
- )
- .filter(**filters)
- .annotate(
- sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- .select_related("project", "workspace", "state", "parent")
- .prefetch_related("assignees", "labels")
- .prefetch_related(
- Prefetch(
- "issue_reactions",
- queryset=IssueReaction.objects.select_related("actor"),
- )
- )
- .order_by("-created_at")
- .annotate(
- link_count=IssueLink.objects.filter(issue=OuterRef("id"))
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- .annotate(
- attachment_count=IssueAttachment.objects.filter(
- issue=OuterRef("id")
- )
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- ).distinct()
-
- # Priority Ordering
- if order_by_param == "priority" or order_by_param == "-priority":
- priority_order = (
- priority_order
- if order_by_param == "priority"
- else priority_order[::-1]
- )
- issue_queryset = issue_queryset.annotate(
- priority_order=Case(
- *[
- When(priority=p, then=Value(i))
- for i, p in enumerate(priority_order)
- ],
- output_field=CharField(),
- )
- ).order_by("priority_order")
-
- # State Ordering
- elif order_by_param in [
- "state__name",
- "state__group",
- "-state__name",
- "-state__group",
- ]:
- state_order = (
- state_order
- if order_by_param in ["state__name", "state__group"]
- else state_order[::-1]
- )
- issue_queryset = issue_queryset.annotate(
- state_order=Case(
- *[
- When(state__group=state_group, then=Value(i))
- for i, state_group in enumerate(state_order)
- ],
- default=Value(len(state_order)),
- output_field=CharField(),
- )
- ).order_by("state_order")
- # assignee and label ordering
- elif order_by_param in [
- "labels__name",
- "-labels__name",
- "assignees__first_name",
- "-assignees__first_name",
- ]:
- issue_queryset = issue_queryset.annotate(
- max_values=Max(
- order_by_param[1::]
- if order_by_param.startswith("-")
- else order_by_param
- )
- ).order_by(
- "-max_values" if order_by_param.startswith("-") else "max_values"
- )
- else:
- issue_queryset = issue_queryset.order_by(order_by_param)
-
- issues = IssueLiteSerializer(issue_queryset, many=True).data
-
- ## Grouping the results
- group_by = request.GET.get("group_by", False)
- if group_by:
- return Response(
- group_results(issues, group_by), status=status.HTTP_200_OK
- )
-
- return Response(issues, status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class WorkspaceLabelsEndpoint(BaseAPIView):
- permission_classes = [
- WorkspaceViewerPermission,
- ]
-
- def get(self, request, slug):
- try:
- labels = Label.objects.filter(
- workspace__slug=slug,
- project__project_projectmember__member=request.user,
- ).values("parent", "name", "color", "id", "project_id", "workspace__slug")
- return Response(labels, status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class WorkspaceMembersEndpoint(BaseAPIView):
- permission_classes = [
- WorkspaceEntityPermission,
- ]
-
- def get(self, request, slug):
- try:
- workspace_members = WorkspaceMember.objects.filter(
- workspace__slug=slug,
- member__is_bot=False,
- ).select_related("workspace", "member")
- serialzier = WorkSpaceMemberSerializer(workspace_members, many=True)
- return Response(serialzier.data, status=status.HTTP_200_OK)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
-
-class LeaveWorkspaceEndpoint(BaseAPIView):
- permission_classes = [
- WorkspaceEntityPermission,
- ]
-
- def delete(self, request, slug):
- try:
- workspace_member = WorkspaceMember.objects.get(
- workspace__slug=slug, member=request.user
- )
-
- # Only Admin case
- if (
- workspace_member.role == 20
- and WorkspaceMember.objects.filter(
- workspace__slug=slug, role=20
- ).count()
- == 1
- ):
- return Response(
- {
- "error": "You cannot leave the workspace since you are the only admin of the workspace you should delete the workspace"
- },
- status=status.HTTP_400_BAD_REQUEST,
- )
- # Delete the member from workspace
- workspace_member.delete()
- return Response(status=status.HTTP_204_NO_CONTENT)
- except WorkspaceMember.DoesNotExist:
- return Response(
- {"error": "Workspace member does not exists"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
diff --git a/apiserver/plane/app/__init__.py b/apiserver/plane/app/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/apiserver/plane/app/apps.py b/apiserver/plane/app/apps.py
new file mode 100644
index 0000000000..e3277fc4d8
--- /dev/null
+++ b/apiserver/plane/app/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class AppApiConfig(AppConfig):
+ name = "plane.app"
diff --git a/apiserver/plane/app/middleware/__init__.py b/apiserver/plane/app/middleware/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/apiserver/plane/app/middleware/api_authentication.py b/apiserver/plane/app/middleware/api_authentication.py
new file mode 100644
index 0000000000..ddabb4132d
--- /dev/null
+++ b/apiserver/plane/app/middleware/api_authentication.py
@@ -0,0 +1,47 @@
+# Django imports
+from django.utils import timezone
+from django.db.models import Q
+
+# Third party imports
+from rest_framework import authentication
+from rest_framework.exceptions import AuthenticationFailed
+
+# Module imports
+from plane.db.models import APIToken
+
+
+class APIKeyAuthentication(authentication.BaseAuthentication):
+ """
+ Authentication with an API Key
+ """
+
+ www_authenticate_realm = "api"
+ media_type = "application/json"
+ auth_header_name = "X-Api-Key"
+
+ def get_api_token(self, request):
+ return request.headers.get(self.auth_header_name)
+
+ def validate_api_token(self, token):
+ try:
+ api_token = APIToken.objects.get(
+ Q(Q(expired_at__gt=timezone.now()) | Q(expired_at__isnull=True)),
+ token=token,
+ is_active=True,
+ )
+ except APIToken.DoesNotExist:
+ raise AuthenticationFailed("Given API token is not valid")
+
+ # save api token last used
+ api_token.last_used = timezone.now()
+ api_token.save(update_fields=["last_used"])
+ return (api_token.user, api_token.token)
+
+ def authenticate(self, request):
+ token = self.get_api_token(request=request)
+ if not token:
+ return None
+
+ # Validate the API token
+ user, token = self.validate_api_token(token)
+ return user, token
diff --git a/apiserver/plane/app/permissions/__init__.py b/apiserver/plane/app/permissions/__init__.py
new file mode 100644
index 0000000000..2298f34428
--- /dev/null
+++ b/apiserver/plane/app/permissions/__init__.py
@@ -0,0 +1,17 @@
+
+from .workspace import (
+ WorkSpaceBasePermission,
+ WorkspaceOwnerPermission,
+ WorkSpaceAdminPermission,
+ WorkspaceEntityPermission,
+ WorkspaceViewerPermission,
+ WorkspaceUserPermission,
+)
+from .project import (
+ ProjectBasePermission,
+ ProjectEntityPermission,
+ ProjectMemberPermission,
+ ProjectLitePermission,
+)
+
+
diff --git a/apiserver/plane/api/permissions/project.py b/apiserver/plane/app/permissions/project.py
similarity index 87%
rename from apiserver/plane/api/permissions/project.py
rename to apiserver/plane/app/permissions/project.py
index e4e3e0f9bc..80775cbf68 100644
--- a/apiserver/plane/api/permissions/project.py
+++ b/apiserver/plane/app/permissions/project.py
@@ -13,14 +13,15 @@ Guest = 5
class ProjectBasePermission(BasePermission):
def has_permission(self, request, view):
-
if request.user.is_anonymous:
return False
## Safe Methods -> Handle the filtering logic in queryset
if request.method in SAFE_METHODS:
return WorkspaceMember.objects.filter(
- workspace__slug=view.workspace_slug, member=request.user
+ workspace__slug=view.workspace_slug,
+ member=request.user,
+ is_active=True,
).exists()
## Only workspace owners or admins can create the projects
@@ -29,6 +30,7 @@ class ProjectBasePermission(BasePermission):
workspace__slug=view.workspace_slug,
member=request.user,
role__in=[Admin, Member],
+ is_active=True,
).exists()
## Only Project Admins can update project attributes
@@ -37,19 +39,21 @@ class ProjectBasePermission(BasePermission):
member=request.user,
role=Admin,
project_id=view.project_id,
+ is_active=True,
).exists()
class ProjectMemberPermission(BasePermission):
def has_permission(self, request, view):
-
if request.user.is_anonymous:
return False
## Safe Methods -> Handle the filtering logic in queryset
if request.method in SAFE_METHODS:
return ProjectMember.objects.filter(
- workspace__slug=view.workspace_slug, member=request.user
+ workspace__slug=view.workspace_slug,
+ member=request.user,
+ is_active=True,
).exists()
## Only workspace owners or admins can create the projects
if request.method == "POST":
@@ -57,6 +61,7 @@ class ProjectMemberPermission(BasePermission):
workspace__slug=view.workspace_slug,
member=request.user,
role__in=[Admin, Member],
+ is_active=True,
).exists()
## Only Project Admins can update project attributes
@@ -65,12 +70,12 @@ class ProjectMemberPermission(BasePermission):
member=request.user,
role__in=[Admin, Member],
project_id=view.project_id,
+ is_active=True,
).exists()
class ProjectEntityPermission(BasePermission):
def has_permission(self, request, view):
-
if request.user.is_anonymous:
return False
@@ -80,6 +85,7 @@ class ProjectEntityPermission(BasePermission):
workspace__slug=view.workspace_slug,
member=request.user,
project_id=view.project_id,
+ is_active=True,
).exists()
## Only project members or admins can create and edit the project attributes
@@ -88,17 +94,18 @@ class ProjectEntityPermission(BasePermission):
member=request.user,
role__in=[Admin, Member],
project_id=view.project_id,
+ is_active=True,
).exists()
class ProjectLitePermission(BasePermission):
-
def has_permission(self, request, view):
if request.user.is_anonymous:
return False
-
+
return ProjectMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
project_id=view.project_id,
- ).exists()
\ No newline at end of file
+ is_active=True,
+ ).exists()
diff --git a/apiserver/plane/api/permissions/workspace.py b/apiserver/plane/app/permissions/workspace.py
similarity index 68%
rename from apiserver/plane/api/permissions/workspace.py
rename to apiserver/plane/app/permissions/workspace.py
index 66e8366146..f73ae1f67e 100644
--- a/apiserver/plane/api/permissions/workspace.py
+++ b/apiserver/plane/app/permissions/workspace.py
@@ -32,15 +32,31 @@ class WorkSpaceBasePermission(BasePermission):
member=request.user,
workspace__slug=view.workspace_slug,
role__in=[Owner, Admin],
+ is_active=True,
).exists()
# allow only owner to delete the workspace
if request.method == "DELETE":
return WorkspaceMember.objects.filter(
- member=request.user, workspace__slug=view.workspace_slug, role=Owner
+ member=request.user,
+ workspace__slug=view.workspace_slug,
+ role=Owner,
+ is_active=True,
).exists()
+class WorkspaceOwnerPermission(BasePermission):
+ def has_permission(self, request, view):
+ if request.user.is_anonymous:
+ return False
+
+ return WorkspaceMember.objects.filter(
+ workspace__slug=view.workspace_slug,
+ member=request.user,
+ role=Owner,
+ ).exists()
+
+
class WorkSpaceAdminPermission(BasePermission):
def has_permission(self, request, view):
if request.user.is_anonymous:
@@ -50,6 +66,7 @@ class WorkSpaceAdminPermission(BasePermission):
member=request.user,
workspace__slug=view.workspace_slug,
role__in=[Owner, Admin],
+ is_active=True,
).exists()
@@ -63,12 +80,14 @@ class WorkspaceEntityPermission(BasePermission):
return WorkspaceMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
+ is_active=True,
).exists()
return WorkspaceMember.objects.filter(
member=request.user,
workspace__slug=view.workspace_slug,
role__in=[Owner, Admin],
+ is_active=True,
).exists()
@@ -78,5 +97,19 @@ class WorkspaceViewerPermission(BasePermission):
return False
return WorkspaceMember.objects.filter(
- member=request.user, workspace__slug=view.workspace_slug, role__gte=10
+ member=request.user,
+ workspace__slug=view.workspace_slug,
+ is_active=True,
+ ).exists()
+
+
+class WorkspaceUserPermission(BasePermission):
+ def has_permission(self, request, view):
+ if request.user.is_anonymous:
+ return False
+
+ return WorkspaceMember.objects.filter(
+ member=request.user,
+ workspace__slug=view.workspace_slug,
+ is_active=True,
).exists()
diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py
new file mode 100644
index 0000000000..c406453b72
--- /dev/null
+++ b/apiserver/plane/app/serializers/__init__.py
@@ -0,0 +1,104 @@
+from .base import BaseSerializer
+from .user import (
+ UserSerializer,
+ UserLiteSerializer,
+ ChangePasswordSerializer,
+ ResetPasswordSerializer,
+ UserAdminLiteSerializer,
+ UserMeSerializer,
+ UserMeSettingsSerializer,
+)
+from .workspace import (
+ WorkSpaceSerializer,
+ WorkSpaceMemberSerializer,
+ TeamSerializer,
+ WorkSpaceMemberInviteSerializer,
+ WorkspaceLiteSerializer,
+ WorkspaceThemeSerializer,
+ WorkspaceMemberAdminSerializer,
+ WorkspaceMemberMeSerializer,
+)
+from .project import (
+ ProjectSerializer,
+ ProjectListSerializer,
+ ProjectDetailSerializer,
+ ProjectMemberSerializer,
+ ProjectMemberInviteSerializer,
+ ProjectIdentifierSerializer,
+ ProjectFavoriteSerializer,
+ ProjectLiteSerializer,
+ ProjectMemberLiteSerializer,
+ ProjectDeployBoardSerializer,
+ ProjectMemberAdminSerializer,
+ ProjectPublicMemberSerializer,
+)
+from .state import StateSerializer, StateLiteSerializer
+from .view import GlobalViewSerializer, IssueViewSerializer, IssueViewFavoriteSerializer
+from .cycle import (
+ CycleSerializer,
+ CycleIssueSerializer,
+ CycleFavoriteSerializer,
+ CycleWriteSerializer,
+)
+from .asset import FileAssetSerializer
+from .issue import (
+ IssueCreateSerializer,
+ IssueActivitySerializer,
+ IssueCommentSerializer,
+ IssuePropertySerializer,
+ IssueAssigneeSerializer,
+ LabelSerializer,
+ IssueSerializer,
+ IssueFlatSerializer,
+ IssueStateSerializer,
+ IssueLinkSerializer,
+ IssueLiteSerializer,
+ IssueAttachmentSerializer,
+ IssueSubscriberSerializer,
+ IssueReactionSerializer,
+ CommentReactionSerializer,
+ IssueVoteSerializer,
+ IssueRelationSerializer,
+ RelatedIssueSerializer,
+ IssuePublicSerializer,
+)
+
+from .module import (
+ ModuleWriteSerializer,
+ ModuleSerializer,
+ ModuleIssueSerializer,
+ ModuleLinkSerializer,
+ ModuleFavoriteSerializer,
+)
+
+from .api import APITokenSerializer, APITokenReadSerializer
+
+from .integration import (
+ IntegrationSerializer,
+ WorkspaceIntegrationSerializer,
+ GithubIssueSyncSerializer,
+ GithubRepositorySerializer,
+ GithubRepositorySyncSerializer,
+ GithubCommentSyncSerializer,
+ SlackProjectSyncSerializer,
+)
+
+from .importer import ImporterSerializer
+
+from .page import PageSerializer, PageLogSerializer, SubPageSerializer, PageFavoriteSerializer
+
+from .estimate import (
+ EstimateSerializer,
+ EstimatePointSerializer,
+ EstimateReadSerializer,
+)
+
+from .inbox import InboxSerializer, InboxIssueSerializer, IssueStateInboxSerializer
+
+from .analytic import AnalyticViewSerializer
+
+from .notification import NotificationSerializer
+
+from .exporter import ExporterHistorySerializer
+
+from .webhook import WebhookSerializer, WebhookLogSerializer
\ No newline at end of file
diff --git a/apiserver/plane/api/serializers/analytic.py b/apiserver/plane/app/serializers/analytic.py
similarity index 91%
rename from apiserver/plane/api/serializers/analytic.py
rename to apiserver/plane/app/serializers/analytic.py
index 5f35e11178..9f3ee6d0a2 100644
--- a/apiserver/plane/api/serializers/analytic.py
+++ b/apiserver/plane/app/serializers/analytic.py
@@ -17,7 +17,7 @@ class AnalyticViewSerializer(BaseSerializer):
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
- validated_data["query"] = dict()
+ validated_data["query"] = {}
return AnalyticView.objects.create(**validated_data)
def update(self, instance, validated_data):
@@ -25,6 +25,6 @@ class AnalyticViewSerializer(BaseSerializer):
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
- validated_data["query"] = dict()
+ validated_data["query"] = {}
validated_data["query"] = issue_filters(query_params, "PATCH")
return super().update(instance, validated_data)
diff --git a/apiserver/plane/app/serializers/api.py b/apiserver/plane/app/serializers/api.py
new file mode 100644
index 0000000000..08bb747d9b
--- /dev/null
+++ b/apiserver/plane/app/serializers/api.py
@@ -0,0 +1,31 @@
+from .base import BaseSerializer
+from plane.db.models import APIToken, APIActivityLog
+
+
+class APITokenSerializer(BaseSerializer):
+
+ class Meta:
+ model = APIToken
+ fields = "__all__"
+ read_only_fields = [
+ "token",
+ "expired_at",
+ "created_at",
+ "updated_at",
+ "workspace",
+ "user",
+ ]
+
+
+class APITokenReadSerializer(BaseSerializer):
+
+ class Meta:
+ model = APIToken
+ exclude = ('token',)
+
+
+class APIActivityLogSerializer(BaseSerializer):
+
+ class Meta:
+ model = APIActivityLog
+ fields = "__all__"
diff --git a/apiserver/plane/api/serializers/asset.py b/apiserver/plane/app/serializers/asset.py
similarity index 100%
rename from apiserver/plane/api/serializers/asset.py
rename to apiserver/plane/app/serializers/asset.py
diff --git a/apiserver/plane/app/serializers/base.py b/apiserver/plane/app/serializers/base.py
new file mode 100644
index 0000000000..89c9725d95
--- /dev/null
+++ b/apiserver/plane/app/serializers/base.py
@@ -0,0 +1,58 @@
+from rest_framework import serializers
+
+
+class BaseSerializer(serializers.ModelSerializer):
+ id = serializers.PrimaryKeyRelatedField(read_only=True)
+
+class DynamicBaseSerializer(BaseSerializer):
+
+ def __init__(self, *args, **kwargs):
+ # If 'fields' is provided in the arguments, remove it and store it separately.
+ # This is done so as not to pass this custom argument up to the superclass.
+ fields = kwargs.pop("fields", None)
+
+ # Call the initialization of the superclass.
+ super().__init__(*args, **kwargs)
+
+ # If 'fields' was provided, filter the fields of the serializer accordingly.
+ if fields is not None:
+ self.fields = self._filter_fields(fields)
+
+ def _filter_fields(self, fields):
+ """
+ Adjust the serializer's fields based on the provided 'fields' list.
+
+ :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.
+ for field_name in fields:
+ # If the field is a dictionary (indicating nested fields),
+ # loop through its keys and values.
+ if isinstance(field_name, dict):
+ for key, value in field_name.items():
+ # If the value of this nested field is a list,
+ # perform a recursive filter on it.
+ if isinstance(value, list):
+ self._filter_fields(self.fields[key], value)
+
+ # Create a list to store allowed fields.
+ allowed = []
+ for item in fields:
+ # If the item is a string, it directly represents a field's name.
+ if isinstance(item, str):
+ allowed.append(item)
+ # If the item is a dictionary, it represents a nested field.
+ # Add the key of this dictionary to the allowed list.
+ elif isinstance(item, dict):
+ allowed.append(list(item.keys())[0])
+
+ # Convert the current serializer's fields and the allowed fields to sets.
+ existing = set(self.fields)
+ allowed = set(allowed)
+
+ # Remove fields from the serializer that aren't in the 'allowed' list.
+ for field_name in (existing - allowed):
+ self.fields.pop(field_name)
+
+ return self.fields
diff --git a/apiserver/plane/app/serializers/cycle.py b/apiserver/plane/app/serializers/cycle.py
new file mode 100644
index 0000000000..104a3dd067
--- /dev/null
+++ b/apiserver/plane/app/serializers/cycle.py
@@ -0,0 +1,107 @@
+# Third party imports
+from rest_framework import serializers
+
+# Module imports
+from .base import BaseSerializer
+from .user import UserLiteSerializer
+from .issue import IssueStateSerializer
+from .workspace import WorkspaceLiteSerializer
+from .project import ProjectLiteSerializer
+from plane.db.models import Cycle, CycleIssue, CycleFavorite
+
+
+class CycleWriteSerializer(BaseSerializer):
+ def validate(self, data):
+ if (
+ data.get("start_date", None) is not None
+ and data.get("end_date", None) is not None
+ and data.get("start_date", None) > data.get("end_date", None)
+ ):
+ raise serializers.ValidationError("Start date cannot exceed end date")
+ return data
+
+ class Meta:
+ model = Cycle
+ fields = "__all__"
+
+
+class CycleSerializer(BaseSerializer):
+ owned_by = UserLiteSerializer(read_only=True)
+ is_favorite = serializers.BooleanField(read_only=True)
+ total_issues = serializers.IntegerField(read_only=True)
+ cancelled_issues = serializers.IntegerField(read_only=True)
+ completed_issues = serializers.IntegerField(read_only=True)
+ started_issues = serializers.IntegerField(read_only=True)
+ unstarted_issues = serializers.IntegerField(read_only=True)
+ backlog_issues = serializers.IntegerField(read_only=True)
+ assignees = serializers.SerializerMethodField(read_only=True)
+ total_estimates = serializers.IntegerField(read_only=True)
+ completed_estimates = serializers.IntegerField(read_only=True)
+ started_estimates = serializers.IntegerField(read_only=True)
+ workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
+ project_detail = ProjectLiteSerializer(read_only=True, source="project")
+
+ def validate(self, data):
+ if (
+ data.get("start_date", None) is not None
+ and data.get("end_date", None) is not None
+ and data.get("start_date", None) > data.get("end_date", None)
+ ):
+ raise serializers.ValidationError("Start date cannot exceed end date")
+ return data
+
+ def get_assignees(self, obj):
+ members = [
+ {
+ "avatar": assignee.avatar,
+ "display_name": assignee.display_name,
+ "id": assignee.id,
+ }
+ for issue_cycle in obj.issue_cycle.prefetch_related(
+ "issue__assignees"
+ ).all()
+ for assignee in issue_cycle.issue.assignees.all()
+ ]
+ # Use a set comprehension to return only the unique objects
+ unique_objects = {frozenset(item.items()) for item in members}
+
+ # Convert the set back to a list of dictionaries
+ unique_list = [dict(item) for item in unique_objects]
+
+ return unique_list
+
+ class Meta:
+ model = Cycle
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ "project",
+ "owned_by",
+ ]
+
+
+class CycleIssueSerializer(BaseSerializer):
+ issue_detail = IssueStateSerializer(read_only=True, source="issue")
+ sub_issues_count = serializers.IntegerField(read_only=True)
+
+ class Meta:
+ model = CycleIssue
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ "project",
+ "cycle",
+ ]
+
+
+class CycleFavoriteSerializer(BaseSerializer):
+ cycle_detail = CycleSerializer(source="cycle", read_only=True)
+
+ class Meta:
+ model = CycleFavorite
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ "project",
+ "user",
+ ]
diff --git a/apiserver/plane/api/serializers/estimate.py b/apiserver/plane/app/serializers/estimate.py
similarity index 94%
rename from apiserver/plane/api/serializers/estimate.py
rename to apiserver/plane/app/serializers/estimate.py
index 3cb0e4713a..4a1cda7792 100644
--- a/apiserver/plane/api/serializers/estimate.py
+++ b/apiserver/plane/app/serializers/estimate.py
@@ -2,7 +2,7 @@
from .base import BaseSerializer
from plane.db.models import Estimate, EstimatePoint
-from plane.api.serializers import WorkspaceLiteSerializer, ProjectLiteSerializer
+from plane.app.serializers import WorkspaceLiteSerializer, ProjectLiteSerializer
class EstimateSerializer(BaseSerializer):
diff --git a/apiserver/plane/api/serializers/exporter.py b/apiserver/plane/app/serializers/exporter.py
similarity index 100%
rename from apiserver/plane/api/serializers/exporter.py
rename to apiserver/plane/app/serializers/exporter.py
diff --git a/apiserver/plane/api/serializers/importer.py b/apiserver/plane/app/serializers/importer.py
similarity index 100%
rename from apiserver/plane/api/serializers/importer.py
rename to apiserver/plane/app/serializers/importer.py
diff --git a/apiserver/plane/app/serializers/inbox.py b/apiserver/plane/app/serializers/inbox.py
new file mode 100644
index 0000000000..f52a90660b
--- /dev/null
+++ b/apiserver/plane/app/serializers/inbox.py
@@ -0,0 +1,57 @@
+# Third party frameworks
+from rest_framework import serializers
+
+# Module imports
+from .base import BaseSerializer
+from .issue import IssueFlatSerializer, LabelLiteSerializer
+from .project import ProjectLiteSerializer
+from .state import StateLiteSerializer
+from .user import UserLiteSerializer
+from plane.db.models import Inbox, InboxIssue, Issue
+
+
+class InboxSerializer(BaseSerializer):
+ project_detail = ProjectLiteSerializer(source="project", read_only=True)
+ pending_issue_count = serializers.IntegerField(read_only=True)
+
+ class Meta:
+ model = Inbox
+ fields = "__all__"
+ read_only_fields = [
+ "project",
+ "workspace",
+ ]
+
+
+class InboxIssueSerializer(BaseSerializer):
+ issue_detail = IssueFlatSerializer(source="issue", read_only=True)
+ project_detail = ProjectLiteSerializer(source="project", read_only=True)
+
+ class Meta:
+ model = InboxIssue
+ fields = "__all__"
+ read_only_fields = [
+ "project",
+ "workspace",
+ ]
+
+
+class InboxIssueLiteSerializer(BaseSerializer):
+ class Meta:
+ model = InboxIssue
+ fields = ["id", "status", "duplicate_to", "snoozed_till", "source"]
+ read_only_fields = fields
+
+
+class IssueStateInboxSerializer(BaseSerializer):
+ state_detail = StateLiteSerializer(read_only=True, source="state")
+ project_detail = ProjectLiteSerializer(read_only=True, source="project")
+ label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
+ assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
+ sub_issues_count = serializers.IntegerField(read_only=True)
+ bridge_id = serializers.UUIDField(read_only=True)
+ issue_inbox = InboxIssueLiteSerializer(read_only=True, many=True)
+
+ class Meta:
+ model = Issue
+ fields = "__all__"
diff --git a/apiserver/plane/api/serializers/integration/__init__.py b/apiserver/plane/app/serializers/integration/__init__.py
similarity index 83%
rename from apiserver/plane/api/serializers/integration/__init__.py
rename to apiserver/plane/app/serializers/integration/__init__.py
index 963fc295e2..112ff02d16 100644
--- a/apiserver/plane/api/serializers/integration/__init__.py
+++ b/apiserver/plane/app/serializers/integration/__init__.py
@@ -5,4 +5,4 @@ from .github import (
GithubIssueSyncSerializer,
GithubCommentSyncSerializer,
)
-from .slack import SlackProjectSyncSerializer
\ No newline at end of file
+from .slack import SlackProjectSyncSerializer
diff --git a/apiserver/plane/api/serializers/integration/base.py b/apiserver/plane/app/serializers/integration/base.py
similarity index 90%
rename from apiserver/plane/api/serializers/integration/base.py
rename to apiserver/plane/app/serializers/integration/base.py
index 10ebd46201..6f6543b9ee 100644
--- a/apiserver/plane/api/serializers/integration/base.py
+++ b/apiserver/plane/app/serializers/integration/base.py
@@ -1,5 +1,5 @@
# Module imports
-from plane.api.serializers import BaseSerializer
+from plane.app.serializers import BaseSerializer
from plane.db.models import Integration, WorkspaceIntegration
diff --git a/apiserver/plane/api/serializers/integration/github.py b/apiserver/plane/app/serializers/integration/github.py
similarity index 95%
rename from apiserver/plane/api/serializers/integration/github.py
rename to apiserver/plane/app/serializers/integration/github.py
index 8352dcee14..850bccf1b3 100644
--- a/apiserver/plane/api/serializers/integration/github.py
+++ b/apiserver/plane/app/serializers/integration/github.py
@@ -1,5 +1,5 @@
# Module imports
-from plane.api.serializers import BaseSerializer
+from plane.app.serializers import BaseSerializer
from plane.db.models import (
GithubIssueSync,
GithubRepository,
diff --git a/apiserver/plane/api/serializers/integration/slack.py b/apiserver/plane/app/serializers/integration/slack.py
similarity index 86%
rename from apiserver/plane/api/serializers/integration/slack.py
rename to apiserver/plane/app/serializers/integration/slack.py
index f535a64de1..9c461c5b9b 100644
--- a/apiserver/plane/api/serializers/integration/slack.py
+++ b/apiserver/plane/app/serializers/integration/slack.py
@@ -1,5 +1,5 @@
# Module imports
-from plane.api.serializers import BaseSerializer
+from plane.app.serializers import BaseSerializer
from plane.db.models import SlackProjectSync
diff --git a/apiserver/plane/app/serializers/issue.py b/apiserver/plane/app/serializers/issue.py
new file mode 100644
index 0000000000..b13d03e35a
--- /dev/null
+++ b/apiserver/plane/app/serializers/issue.py
@@ -0,0 +1,616 @@
+# Django imports
+from django.utils import timezone
+
+# Third Party imports
+from rest_framework import serializers
+
+# Module imports
+from .base import BaseSerializer, DynamicBaseSerializer
+from .user import UserLiteSerializer
+from .state import StateSerializer, StateLiteSerializer
+from .project import ProjectLiteSerializer
+from .workspace import WorkspaceLiteSerializer
+from plane.db.models import (
+ User,
+ Issue,
+ IssueActivity,
+ IssueComment,
+ IssueProperty,
+ IssueAssignee,
+ IssueSubscriber,
+ IssueLabel,
+ Label,
+ CycleIssue,
+ Cycle,
+ Module,
+ ModuleIssue,
+ IssueLink,
+ IssueAttachment,
+ IssueReaction,
+ CommentReaction,
+ IssueVote,
+ IssueRelation,
+)
+
+
+class IssueFlatSerializer(BaseSerializer):
+ ## Contain only flat fields
+
+ class Meta:
+ model = Issue
+ fields = [
+ "id",
+ "name",
+ "description",
+ "description_html",
+ "priority",
+ "start_date",
+ "target_date",
+ "sequence_id",
+ "sort_order",
+ "is_draft",
+ ]
+
+
+class IssueProjectLiteSerializer(BaseSerializer):
+ project_detail = ProjectLiteSerializer(source="project", read_only=True)
+
+ class Meta:
+ model = Issue
+ fields = [
+ "id",
+ "project_detail",
+ "name",
+ "sequence_id",
+ ]
+ read_only_fields = fields
+
+
+##TODO: Find a better way to write this serializer
+## Find a better approach to save manytomany?
+class IssueCreateSerializer(BaseSerializer):
+ state_detail = StateSerializer(read_only=True, source="state")
+ created_by_detail = UserLiteSerializer(read_only=True, source="created_by")
+ project_detail = ProjectLiteSerializer(read_only=True, source="project")
+ workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
+
+ assignees = serializers.ListField(
+ child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
+ write_only=True,
+ required=False,
+ )
+
+ labels = serializers.ListField(
+ child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
+ write_only=True,
+ required=False,
+ )
+
+ class Meta:
+ model = Issue
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ "project",
+ "created_by",
+ "updated_by",
+ "created_at",
+ "updated_at",
+ ]
+
+ def to_representation(self, instance):
+ data = super().to_representation(instance)
+ data['assignees'] = [str(assignee.id) for assignee in instance.assignees.all()]
+ data['labels'] = [str(label.id) for label in instance.labels.all()]
+ return data
+
+ def validate(self, data):
+ if (
+ data.get("start_date", None) is not None
+ and data.get("target_date", None) is not None
+ and data.get("start_date", None) > data.get("target_date", None)
+ ):
+ raise serializers.ValidationError("Start date cannot exceed target date")
+ return data
+
+ def create(self, validated_data):
+ assignees = validated_data.pop("assignees", None)
+ labels = validated_data.pop("labels", None)
+
+ project_id = self.context["project_id"]
+ workspace_id = self.context["workspace_id"]
+ default_assignee_id = self.context["default_assignee_id"]
+
+ issue = Issue.objects.create(**validated_data, project_id=project_id)
+
+ # Issue Audit Users
+ created_by_id = issue.created_by_id
+ updated_by_id = issue.updated_by_id
+
+ if assignees is not None and len(assignees):
+ IssueAssignee.objects.bulk_create(
+ [
+ IssueAssignee(
+ assignee=user,
+ issue=issue,
+ project_id=project_id,
+ workspace_id=workspace_id,
+ created_by_id=created_by_id,
+ updated_by_id=updated_by_id,
+ )
+ for user in assignees
+ ],
+ batch_size=10,
+ )
+ else:
+ # Then assign it to default assignee
+ if default_assignee_id is not None:
+ IssueAssignee.objects.create(
+ assignee_id=default_assignee_id,
+ issue=issue,
+ project_id=project_id,
+ workspace_id=workspace_id,
+ created_by_id=created_by_id,
+ updated_by_id=updated_by_id,
+ )
+
+ if labels is not None and len(labels):
+ IssueLabel.objects.bulk_create(
+ [
+ IssueLabel(
+ label=label,
+ issue=issue,
+ project_id=project_id,
+ workspace_id=workspace_id,
+ created_by_id=created_by_id,
+ updated_by_id=updated_by_id,
+ )
+ for label in labels
+ ],
+ batch_size=10,
+ )
+
+ return issue
+
+ def update(self, instance, validated_data):
+ assignees = validated_data.pop("assignees", None)
+ labels = validated_data.pop("labels", None)
+
+ # Related models
+ project_id = instance.project_id
+ workspace_id = instance.workspace_id
+ created_by_id = instance.created_by_id
+ updated_by_id = instance.updated_by_id
+
+ if assignees is not None:
+ IssueAssignee.objects.filter(issue=instance).delete()
+ IssueAssignee.objects.bulk_create(
+ [
+ IssueAssignee(
+ assignee=user,
+ issue=instance,
+ project_id=project_id,
+ workspace_id=workspace_id,
+ created_by_id=created_by_id,
+ updated_by_id=updated_by_id,
+ )
+ for user in assignees
+ ],
+ batch_size=10,
+ )
+
+ if labels is not None:
+ IssueLabel.objects.filter(issue=instance).delete()
+ IssueLabel.objects.bulk_create(
+ [
+ IssueLabel(
+ label=label,
+ issue=instance,
+ project_id=project_id,
+ workspace_id=workspace_id,
+ created_by_id=created_by_id,
+ updated_by_id=updated_by_id,
+ )
+ for label in labels
+ ],
+ batch_size=10,
+ )
+
+ # Time updation occues even when other related models are updated
+ instance.updated_at = timezone.now()
+ return super().update(instance, validated_data)
+
+
+class IssueActivitySerializer(BaseSerializer):
+ actor_detail = UserLiteSerializer(read_only=True, source="actor")
+ issue_detail = IssueFlatSerializer(read_only=True, source="issue")
+ project_detail = ProjectLiteSerializer(read_only=True, source="project")
+ workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
+
+ class Meta:
+ model = IssueActivity
+ fields = "__all__"
+
+
+
+class IssuePropertySerializer(BaseSerializer):
+ class Meta:
+ model = IssueProperty
+ fields = "__all__"
+ read_only_fields = [
+ "user",
+ "workspace",
+ "project",
+ ]
+
+
+class LabelSerializer(BaseSerializer):
+ workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
+ project_detail = ProjectLiteSerializer(source="project", read_only=True)
+
+ class Meta:
+ model = Label
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ "project",
+ ]
+
+
+class LabelLiteSerializer(BaseSerializer):
+ class Meta:
+ model = Label
+ fields = [
+ "id",
+ "name",
+ "color",
+ ]
+
+
+class IssueLabelSerializer(BaseSerializer):
+
+ class Meta:
+ model = IssueLabel
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ "project",
+ ]
+
+
+class IssueRelationSerializer(BaseSerializer):
+ issue_detail = IssueProjectLiteSerializer(read_only=True, source="related_issue")
+
+ class Meta:
+ model = IssueRelation
+ fields = [
+ "issue_detail",
+ "relation_type",
+ "related_issue",
+ "issue",
+ "id"
+ ]
+ read_only_fields = [
+ "workspace",
+ "project",
+ ]
+
+class RelatedIssueSerializer(BaseSerializer):
+ issue_detail = IssueProjectLiteSerializer(read_only=True, source="issue")
+
+ class Meta:
+ model = IssueRelation
+ fields = [
+ "issue_detail",
+ "relation_type",
+ "related_issue",
+ "issue",
+ "id"
+ ]
+ read_only_fields = [
+ "workspace",
+ "project",
+ ]
+
+
+class IssueAssigneeSerializer(BaseSerializer):
+ assignee_details = UserLiteSerializer(read_only=True, source="assignee")
+
+ class Meta:
+ model = IssueAssignee
+ fields = "__all__"
+
+
+class CycleBaseSerializer(BaseSerializer):
+ class Meta:
+ model = Cycle
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ "project",
+ "created_by",
+ "updated_by",
+ "created_at",
+ "updated_at",
+ ]
+
+
+class IssueCycleDetailSerializer(BaseSerializer):
+ cycle_detail = CycleBaseSerializer(read_only=True, source="cycle")
+
+ class Meta:
+ model = CycleIssue
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ "project",
+ "created_by",
+ "updated_by",
+ "created_at",
+ "updated_at",
+ ]
+
+
+class ModuleBaseSerializer(BaseSerializer):
+ class Meta:
+ model = Module
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ "project",
+ "created_by",
+ "updated_by",
+ "created_at",
+ "updated_at",
+ ]
+
+
+class IssueModuleDetailSerializer(BaseSerializer):
+ module_detail = ModuleBaseSerializer(read_only=True, source="module")
+
+ class Meta:
+ model = ModuleIssue
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ "project",
+ "created_by",
+ "updated_by",
+ "created_at",
+ "updated_at",
+ ]
+
+
+class IssueLinkSerializer(BaseSerializer):
+ created_by_detail = UserLiteSerializer(read_only=True, source="created_by")
+
+ class Meta:
+ model = IssueLink
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ "project",
+ "created_by",
+ "updated_by",
+ "created_at",
+ "updated_at",
+ "issue",
+ ]
+
+ # Validation if url already exists
+ def create(self, validated_data):
+ if IssueLink.objects.filter(
+ url=validated_data.get("url"), issue_id=validated_data.get("issue_id")
+ ).exists():
+ raise serializers.ValidationError(
+ {"error": "URL already exists for this Issue"}
+ )
+ return IssueLink.objects.create(**validated_data)
+
+
+class IssueAttachmentSerializer(BaseSerializer):
+ class Meta:
+ model = IssueAttachment
+ fields = "__all__"
+ read_only_fields = [
+ "created_by",
+ "updated_by",
+ "created_at",
+ "updated_at",
+ "workspace",
+ "project",
+ "issue",
+ ]
+
+
+class IssueReactionSerializer(BaseSerializer):
+
+ actor_detail = UserLiteSerializer(read_only=True, source="actor")
+
+ class Meta:
+ model = IssueReaction
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ "project",
+ "issue",
+ "actor",
+ ]
+
+
+class CommentReactionLiteSerializer(BaseSerializer):
+ actor_detail = UserLiteSerializer(read_only=True, source="actor")
+
+ class Meta:
+ model = CommentReaction
+ fields = [
+ "id",
+ "reaction",
+ "comment",
+ "actor_detail",
+ ]
+
+
+class CommentReactionSerializer(BaseSerializer):
+ class Meta:
+ model = CommentReaction
+ fields = "__all__"
+ read_only_fields = ["workspace", "project", "comment", "actor"]
+
+
+class IssueVoteSerializer(BaseSerializer):
+
+ actor_detail = UserLiteSerializer(read_only=True, source="actor")
+
+ class Meta:
+ model = IssueVote
+ fields = ["issue", "vote", "workspace", "project", "actor", "actor_detail"]
+ read_only_fields = fields
+
+
+class IssueCommentSerializer(BaseSerializer):
+ actor_detail = UserLiteSerializer(read_only=True, source="actor")
+ issue_detail = IssueFlatSerializer(read_only=True, source="issue")
+ project_detail = ProjectLiteSerializer(read_only=True, source="project")
+ workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
+ comment_reactions = CommentReactionLiteSerializer(read_only=True, many=True)
+ is_member = serializers.BooleanField(read_only=True)
+
+ class Meta:
+ model = IssueComment
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ "project",
+ "issue",
+ "created_by",
+ "updated_by",
+ "created_at",
+ "updated_at",
+ ]
+
+
+class IssueStateFlatSerializer(BaseSerializer):
+ state_detail = StateLiteSerializer(read_only=True, source="state")
+ project_detail = ProjectLiteSerializer(read_only=True, source="project")
+
+ class Meta:
+ model = Issue
+ fields = [
+ "id",
+ "sequence_id",
+ "name",
+ "state_detail",
+ "project_detail",
+ ]
+
+
+# Issue Serializer with state details
+class IssueStateSerializer(DynamicBaseSerializer):
+ label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
+ state_detail = StateLiteSerializer(read_only=True, source="state")
+ project_detail = ProjectLiteSerializer(read_only=True, source="project")
+ assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
+ sub_issues_count = serializers.IntegerField(read_only=True)
+ bridge_id = serializers.UUIDField(read_only=True)
+ attachment_count = serializers.IntegerField(read_only=True)
+ link_count = serializers.IntegerField(read_only=True)
+
+ class Meta:
+ model = Issue
+ fields = "__all__"
+
+
+class IssueSerializer(BaseSerializer):
+ project_detail = ProjectLiteSerializer(read_only=True, source="project")
+ state_detail = StateSerializer(read_only=True, source="state")
+ parent_detail = IssueStateFlatSerializer(read_only=True, source="parent")
+ label_details = LabelSerializer(read_only=True, source="labels", many=True)
+ assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
+ related_issues = IssueRelationSerializer(read_only=True, source="issue_relation", many=True)
+ issue_relations = RelatedIssueSerializer(read_only=True, source="issue_related", many=True)
+ issue_cycle = IssueCycleDetailSerializer(read_only=True)
+ issue_module = IssueModuleDetailSerializer(read_only=True)
+ issue_link = IssueLinkSerializer(read_only=True, many=True)
+ issue_attachment = IssueAttachmentSerializer(read_only=True, many=True)
+ sub_issues_count = serializers.IntegerField(read_only=True)
+ issue_reactions = IssueReactionSerializer(read_only=True, many=True)
+
+ class Meta:
+ model = Issue
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ "project",
+ "created_by",
+ "updated_by",
+ "created_at",
+ "updated_at",
+ ]
+
+
+class IssueLiteSerializer(DynamicBaseSerializer):
+ workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
+ project_detail = ProjectLiteSerializer(read_only=True, source="project")
+ state_detail = StateLiteSerializer(read_only=True, source="state")
+ label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
+ assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
+ sub_issues_count = serializers.IntegerField(read_only=True)
+ cycle_id = serializers.UUIDField(read_only=True)
+ module_id = serializers.UUIDField(read_only=True)
+ attachment_count = serializers.IntegerField(read_only=True)
+ link_count = serializers.IntegerField(read_only=True)
+ issue_reactions = IssueReactionSerializer(read_only=True, many=True)
+
+ class Meta:
+ model = Issue
+ fields = "__all__"
+ read_only_fields = [
+ "start_date",
+ "target_date",
+ "completed_at",
+ "workspace",
+ "project",
+ "created_by",
+ "updated_by",
+ "created_at",
+ "updated_at",
+ ]
+
+
+class IssuePublicSerializer(BaseSerializer):
+ project_detail = ProjectLiteSerializer(read_only=True, source="project")
+ state_detail = StateLiteSerializer(read_only=True, source="state")
+ reactions = IssueReactionSerializer(read_only=True, many=True, source="issue_reactions")
+ votes = IssueVoteSerializer(read_only=True, many=True)
+
+ class Meta:
+ model = Issue
+ fields = [
+ "id",
+ "name",
+ "description_html",
+ "sequence_id",
+ "state",
+ "state_detail",
+ "project",
+ "project_detail",
+ "workspace",
+ "priority",
+ "target_date",
+ "reactions",
+ "votes",
+ ]
+ read_only_fields = fields
+
+
+
+class IssueSubscriberSerializer(BaseSerializer):
+ class Meta:
+ model = IssueSubscriber
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ "project",
+ "issue",
+ ]
diff --git a/apiserver/plane/app/serializers/module.py b/apiserver/plane/app/serializers/module.py
new file mode 100644
index 0000000000..48f773b0f8
--- /dev/null
+++ b/apiserver/plane/app/serializers/module.py
@@ -0,0 +1,198 @@
+# Third Party imports
+from rest_framework import serializers
+
+# Module imports
+from .base import BaseSerializer
+from .user import UserLiteSerializer
+from .project import ProjectLiteSerializer
+from .workspace import WorkspaceLiteSerializer
+
+from plane.db.models import (
+ User,
+ Module,
+ ModuleMember,
+ ModuleIssue,
+ ModuleLink,
+ ModuleFavorite,
+)
+
+
+class ModuleWriteSerializer(BaseSerializer):
+ members = serializers.ListField(
+ child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
+ write_only=True,
+ required=False,
+ )
+
+ project_detail = ProjectLiteSerializer(source="project", read_only=True)
+ workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
+
+ class Meta:
+ model = Module
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ "project",
+ "created_by",
+ "updated_by",
+ "created_at",
+ "updated_at",
+ ]
+
+ def to_representation(self, instance):
+ data = super().to_representation(instance)
+ data['members'] = [str(member.id) for member in instance.members.all()]
+ return data
+
+ def validate(self, data):
+ if data.get("start_date", None) is not None and data.get("target_date", None) is not None and data.get("start_date", None) > data.get("target_date", None):
+ raise serializers.ValidationError("Start date cannot exceed target date")
+ return data
+
+ def create(self, validated_data):
+ members = validated_data.pop("members", None)
+
+ project = self.context["project"]
+
+ module = Module.objects.create(**validated_data, project=project)
+
+ if members is not None:
+ ModuleMember.objects.bulk_create(
+ [
+ ModuleMember(
+ module=module,
+ member=member,
+ project=project,
+ workspace=project.workspace,
+ created_by=module.created_by,
+ updated_by=module.updated_by,
+ )
+ for member in members
+ ],
+ batch_size=10,
+ ignore_conflicts=True,
+ )
+
+ return module
+
+ def update(self, instance, validated_data):
+ members = validated_data.pop("members", None)
+
+ if members is not None:
+ ModuleMember.objects.filter(module=instance).delete()
+ ModuleMember.objects.bulk_create(
+ [
+ ModuleMember(
+ module=instance,
+ member=member,
+ project=instance.project,
+ workspace=instance.project.workspace,
+ created_by=instance.created_by,
+ updated_by=instance.updated_by,
+ )
+ for member in members
+ ],
+ batch_size=10,
+ ignore_conflicts=True,
+ )
+
+ return super().update(instance, validated_data)
+
+
+class ModuleFlatSerializer(BaseSerializer):
+ class Meta:
+ model = Module
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ "project",
+ "created_by",
+ "updated_by",
+ "created_at",
+ "updated_at",
+ ]
+
+
+class ModuleIssueSerializer(BaseSerializer):
+ module_detail = ModuleFlatSerializer(read_only=True, source="module")
+ issue_detail = ProjectLiteSerializer(read_only=True, source="issue")
+ sub_issues_count = serializers.IntegerField(read_only=True)
+
+ class Meta:
+ model = ModuleIssue
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ "project",
+ "created_by",
+ "updated_by",
+ "created_at",
+ "updated_at",
+ "module",
+ ]
+
+
+class ModuleLinkSerializer(BaseSerializer):
+ created_by_detail = UserLiteSerializer(read_only=True, source="created_by")
+
+ class Meta:
+ model = ModuleLink
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ "project",
+ "created_by",
+ "updated_by",
+ "created_at",
+ "updated_at",
+ "module",
+ ]
+
+ # Validation if url already exists
+ def create(self, validated_data):
+ if ModuleLink.objects.filter(
+ url=validated_data.get("url"), module_id=validated_data.get("module_id")
+ ).exists():
+ raise serializers.ValidationError(
+ {"error": "URL already exists for this Issue"}
+ )
+ return ModuleLink.objects.create(**validated_data)
+
+
+class ModuleSerializer(BaseSerializer):
+ project_detail = ProjectLiteSerializer(read_only=True, source="project")
+ lead_detail = UserLiteSerializer(read_only=True, source="lead")
+ members_detail = UserLiteSerializer(read_only=True, many=True, source="members")
+ link_module = ModuleLinkSerializer(read_only=True, many=True)
+ is_favorite = serializers.BooleanField(read_only=True)
+ total_issues = serializers.IntegerField(read_only=True)
+ cancelled_issues = serializers.IntegerField(read_only=True)
+ completed_issues = serializers.IntegerField(read_only=True)
+ started_issues = serializers.IntegerField(read_only=True)
+ unstarted_issues = serializers.IntegerField(read_only=True)
+ backlog_issues = serializers.IntegerField(read_only=True)
+
+ class Meta:
+ model = Module
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ "project",
+ "created_by",
+ "updated_by",
+ "created_at",
+ "updated_at",
+ ]
+
+
+class ModuleFavoriteSerializer(BaseSerializer):
+ module_detail = ModuleFlatSerializer(source="module", read_only=True)
+
+ class Meta:
+ model = ModuleFavorite
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ "project",
+ "user",
+ ]
diff --git a/apiserver/plane/api/serializers/notification.py b/apiserver/plane/app/serializers/notification.py
similarity index 100%
rename from apiserver/plane/api/serializers/notification.py
rename to apiserver/plane/app/serializers/notification.py
diff --git a/apiserver/plane/api/serializers/page.py b/apiserver/plane/app/serializers/page.py
similarity index 73%
rename from apiserver/plane/api/serializers/page.py
rename to apiserver/plane/app/serializers/page.py
index 94f7836de1..ff152627a6 100644
--- a/apiserver/plane/api/serializers/page.py
+++ b/apiserver/plane/app/serializers/page.py
@@ -6,39 +6,17 @@ from .base import BaseSerializer
from .issue import IssueFlatSerializer, LabelLiteSerializer
from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer
-from plane.db.models import Page, PageBlock, PageFavorite, PageLabel, Label
-
-
-class PageBlockSerializer(BaseSerializer):
- issue_detail = IssueFlatSerializer(source="issue", read_only=True)
- project_detail = ProjectLiteSerializer(source="project", read_only=True)
- workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
-
- class Meta:
- model = PageBlock
- fields = "__all__"
- read_only_fields = [
- "workspace",
- "project",
- "page",
- ]
-
-class PageBlockLiteSerializer(BaseSerializer):
-
- class Meta:
- model = PageBlock
- fields = "__all__"
+from plane.db.models import Page, PageLog, PageFavorite, PageLabel, Label, Issue, Module
class PageSerializer(BaseSerializer):
is_favorite = serializers.BooleanField(read_only=True)
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
- labels_list = serializers.ListField(
+ labels = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
write_only=True,
required=False,
)
- blocks = PageBlockLiteSerializer(read_only=True, many=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
@@ -50,9 +28,13 @@ class PageSerializer(BaseSerializer):
"project",
"owned_by",
]
+ def to_representation(self, instance):
+ data = super().to_representation(instance)
+ data['labels'] = [str(label.id) for label in instance.labels.all()]
+ return data
def create(self, validated_data):
- labels = validated_data.pop("labels_list", None)
+ labels = validated_data.pop("labels", None)
project_id = self.context["project_id"]
owned_by_id = self.context["owned_by_id"]
page = Page.objects.create(
@@ -77,7 +59,7 @@ class PageSerializer(BaseSerializer):
return page
def update(self, instance, validated_data):
- labels = validated_data.pop("labels_list", None)
+ labels = validated_data.pop("labels", None)
if labels is not None:
PageLabel.objects.filter(page=instance).delete()
PageLabel.objects.bulk_create(
@@ -98,6 +80,41 @@ class PageSerializer(BaseSerializer):
return super().update(instance, validated_data)
+class SubPageSerializer(BaseSerializer):
+ entity_details = serializers.SerializerMethodField()
+
+ class Meta:
+ model = PageLog
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ "project",
+ "page",
+ ]
+
+ def get_entity_details(self, obj):
+ entity_name = obj.entity_name
+ if entity_name == 'forward_link' or entity_name == 'back_link':
+ try:
+ page = Page.objects.get(pk=obj.entity_identifier)
+ return PageSerializer(page).data
+ except Page.DoesNotExist:
+ return None
+ return None
+
+
+class PageLogSerializer(BaseSerializer):
+
+ class Meta:
+ model = PageLog
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ "project",
+ "page",
+ ]
+
+
class PageFavoriteSerializer(BaseSerializer):
page_detail = PageSerializer(source="page", read_only=True)
diff --git a/apiserver/plane/app/serializers/project.py b/apiserver/plane/app/serializers/project.py
new file mode 100644
index 0000000000..58a38f1545
--- /dev/null
+++ b/apiserver/plane/app/serializers/project.py
@@ -0,0 +1,217 @@
+# Third party imports
+from rest_framework import serializers
+
+# Module imports
+from .base import BaseSerializer, DynamicBaseSerializer
+from plane.app.serializers.workspace import WorkspaceLiteSerializer
+from plane.app.serializers.user import UserLiteSerializer, UserAdminLiteSerializer
+from plane.db.models import (
+ Project,
+ ProjectMember,
+ ProjectMemberInvite,
+ ProjectIdentifier,
+ ProjectFavorite,
+ ProjectDeployBoard,
+ ProjectPublicMember,
+)
+
+
+class ProjectSerializer(BaseSerializer):
+ workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
+
+ class Meta:
+ model = Project
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ ]
+
+ def create(self, validated_data):
+ identifier = validated_data.get("identifier", "").strip().upper()
+ if identifier == "":
+ raise serializers.ValidationError(detail="Project Identifier is required")
+
+ if ProjectIdentifier.objects.filter(
+ name=identifier, workspace_id=self.context["workspace_id"]
+ ).exists():
+ raise serializers.ValidationError(detail="Project Identifier is taken")
+ project = Project.objects.create(
+ **validated_data, workspace_id=self.context["workspace_id"]
+ )
+ _ = ProjectIdentifier.objects.create(
+ name=project.identifier,
+ project=project,
+ workspace_id=self.context["workspace_id"],
+ )
+ return project
+
+ def update(self, instance, validated_data):
+ identifier = validated_data.get("identifier", "").strip().upper()
+
+ # If identifier is not passed update the project and return
+ if identifier == "":
+ 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
+ project = super().update(instance, validated_data)
+ return project
+
+ # If not same fail update
+ raise serializers.ValidationError(detail="Project Identifier is already taken")
+
+
+class ProjectLiteSerializer(BaseSerializer):
+ class Meta:
+ model = Project
+ fields = [
+ "id",
+ "identifier",
+ "name",
+ "cover_image",
+ "icon_prop",
+ "emoji",
+ "description",
+ ]
+ read_only_fields = fields
+
+
+class ProjectListSerializer(DynamicBaseSerializer):
+ is_favorite = serializers.BooleanField(read_only=True)
+ total_members = serializers.IntegerField(read_only=True)
+ total_cycles = serializers.IntegerField(read_only=True)
+ total_modules = serializers.IntegerField(read_only=True)
+ is_member = serializers.BooleanField(read_only=True)
+ sort_order = serializers.FloatField(read_only=True)
+ member_role = serializers.IntegerField(read_only=True)
+ is_deployed = serializers.BooleanField(read_only=True)
+ members = serializers.SerializerMethodField()
+
+ def get_members(self, obj):
+ project_members = ProjectMember.objects.filter(
+ project_id=obj.id,
+ is_active=True,
+ ).values(
+ "id",
+ "member_id",
+ "member__display_name",
+ "member__avatar",
+ )
+ return list(project_members)
+
+ class Meta:
+ model = Project
+ fields = "__all__"
+
+
+class ProjectDetailSerializer(BaseSerializer):
+ # workspace = WorkSpaceSerializer(read_only=True)
+ default_assignee = UserLiteSerializer(read_only=True)
+ project_lead = UserLiteSerializer(read_only=True)
+ is_favorite = serializers.BooleanField(read_only=True)
+ total_members = serializers.IntegerField(read_only=True)
+ total_cycles = serializers.IntegerField(read_only=True)
+ total_modules = serializers.IntegerField(read_only=True)
+ is_member = serializers.BooleanField(read_only=True)
+ sort_order = serializers.FloatField(read_only=True)
+ member_role = serializers.IntegerField(read_only=True)
+ is_deployed = serializers.BooleanField(read_only=True)
+
+ class Meta:
+ model = Project
+ fields = "__all__"
+
+
+class ProjectMemberSerializer(BaseSerializer):
+ workspace = WorkspaceLiteSerializer(read_only=True)
+ project = ProjectLiteSerializer(read_only=True)
+ member = UserLiteSerializer(read_only=True)
+
+ class Meta:
+ model = ProjectMember
+ fields = "__all__"
+
+
+class ProjectMemberAdminSerializer(BaseSerializer):
+ workspace = WorkspaceLiteSerializer(read_only=True)
+ project = ProjectLiteSerializer(read_only=True)
+ member = UserAdminLiteSerializer(read_only=True)
+
+ class Meta:
+ model = ProjectMember
+ fields = "__all__"
+
+
+class ProjectMemberInviteSerializer(BaseSerializer):
+ project = ProjectLiteSerializer(read_only=True)
+ workspace = WorkspaceLiteSerializer(read_only=True)
+
+ class Meta:
+ model = ProjectMemberInvite
+ fields = "__all__"
+
+
+class ProjectIdentifierSerializer(BaseSerializer):
+ class Meta:
+ model = ProjectIdentifier
+ fields = "__all__"
+
+
+class ProjectFavoriteSerializer(BaseSerializer):
+ class Meta:
+ model = ProjectFavorite
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ "user",
+ ]
+
+
+class ProjectMemberLiteSerializer(BaseSerializer):
+ member = UserLiteSerializer(read_only=True)
+ is_subscribed = serializers.BooleanField(read_only=True)
+
+ class Meta:
+ model = ProjectMember
+ fields = ["member", "id", "is_subscribed"]
+ read_only_fields = fields
+
+
+class ProjectDeployBoardSerializer(BaseSerializer):
+ project_details = ProjectLiteSerializer(read_only=True, source="project")
+ workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
+
+ class Meta:
+ model = ProjectDeployBoard
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ "project",
+ "anchor",
+ ]
+
+
+class ProjectPublicMemberSerializer(BaseSerializer):
+ class Meta:
+ model = ProjectPublicMember
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ "project",
+ "member",
+ ]
\ No newline at end of file
diff --git a/apiserver/plane/app/serializers/state.py b/apiserver/plane/app/serializers/state.py
new file mode 100644
index 0000000000..323254f269
--- /dev/null
+++ b/apiserver/plane/app/serializers/state.py
@@ -0,0 +1,28 @@
+# Module imports
+from .base import BaseSerializer
+
+
+from plane.db.models import State
+
+
+class StateSerializer(BaseSerializer):
+
+ class Meta:
+ model = State
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ "project",
+ ]
+
+
+class StateLiteSerializer(BaseSerializer):
+ class Meta:
+ model = State
+ fields = [
+ "id",
+ "name",
+ "color",
+ "group",
+ ]
+ read_only_fields = fields
\ No newline at end of file
diff --git a/apiserver/plane/app/serializers/user.py b/apiserver/plane/app/serializers/user.py
new file mode 100644
index 0000000000..5c9c69e5ca
--- /dev/null
+++ b/apiserver/plane/app/serializers/user.py
@@ -0,0 +1,195 @@
+# Third party imports
+from rest_framework import serializers
+
+# Module import
+from .base import BaseSerializer
+from plane.db.models import User, Workspace, WorkspaceMemberInvite
+from plane.license.models import InstanceAdmin, Instance
+
+
+class UserSerializer(BaseSerializer):
+ class Meta:
+ model = User
+ fields = "__all__"
+ read_only_fields = [
+ "id",
+ "created_at",
+ "updated_at",
+ "is_superuser",
+ "is_staff",
+ "last_active",
+ "last_login_time",
+ "last_logout_time",
+ "last_login_ip",
+ "last_logout_ip",
+ "last_login_uagent",
+ "token_updated_at",
+ "is_onboarded",
+ "is_bot",
+ "is_password_autoset",
+ "is_email_verified",
+ ]
+ extra_kwargs = {"password": {"write_only": True}}
+
+ # If the user has already filled first name or last name then he is onboarded
+ def get_is_onboarded(self, obj):
+ return bool(obj.first_name) or bool(obj.last_name)
+
+
+class UserMeSerializer(BaseSerializer):
+ class Meta:
+ model = User
+ fields = [
+ "id",
+ "avatar",
+ "cover_image",
+ "date_joined",
+ "display_name",
+ "email",
+ "first_name",
+ "last_name",
+ "is_active",
+ "is_bot",
+ "is_email_verified",
+ "is_managed",
+ "is_onboarded",
+ "is_tour_completed",
+ "mobile_number",
+ "role",
+ "onboarding_step",
+ "user_timezone",
+ "username",
+ "theme",
+ "last_workspace_id",
+ "use_case",
+ "is_password_autoset",
+ "is_email_verified",
+ ]
+ read_only_fields = fields
+
+
+class UserMeSettingsSerializer(BaseSerializer):
+ workspace = serializers.SerializerMethodField()
+
+ class Meta:
+ model = User
+ fields = [
+ "id",
+ "email",
+ "workspace",
+ ]
+ read_only_fields = fields
+
+ def get_workspace(self, obj):
+ workspace_invites = WorkspaceMemberInvite.objects.filter(
+ email=obj.email
+ ).count()
+ if (
+ obj.last_workspace_id is not None
+ and Workspace.objects.filter(
+ pk=obj.last_workspace_id,
+ workspace_member__member=obj.id,
+ workspace_member__is_active=True,
+ ).exists()
+ ):
+ workspace = Workspace.objects.filter(
+ pk=obj.last_workspace_id,
+ workspace_member__member=obj.id,
+ workspace_member__is_active=True,
+ ).first()
+ return {
+ "last_workspace_id": obj.last_workspace_id,
+ "last_workspace_slug": workspace.slug if workspace is not None else "",
+ "fallback_workspace_id": obj.last_workspace_id,
+ "fallback_workspace_slug": workspace.slug
+ if workspace is not None
+ else "",
+ "invites": workspace_invites,
+ }
+ else:
+ fallback_workspace = (
+ Workspace.objects.filter(
+ workspace_member__member_id=obj.id, workspace_member__is_active=True
+ )
+ .order_by("created_at")
+ .first()
+ )
+ return {
+ "last_workspace_id": None,
+ "last_workspace_slug": None,
+ "fallback_workspace_id": fallback_workspace.id
+ if fallback_workspace is not None
+ else None,
+ "fallback_workspace_slug": fallback_workspace.slug
+ if fallback_workspace is not None
+ else None,
+ "invites": workspace_invites,
+ }
+
+
+class UserLiteSerializer(BaseSerializer):
+ class Meta:
+ model = User
+ fields = [
+ "id",
+ "first_name",
+ "last_name",
+ "avatar",
+ "is_bot",
+ "display_name",
+ ]
+ read_only_fields = [
+ "id",
+ "is_bot",
+ ]
+
+
+class UserAdminLiteSerializer(BaseSerializer):
+ class Meta:
+ model = User
+ fields = [
+ "id",
+ "first_name",
+ "last_name",
+ "avatar",
+ "is_bot",
+ "display_name",
+ "email",
+ ]
+ read_only_fields = [
+ "id",
+ "is_bot",
+ ]
+
+
+class ChangePasswordSerializer(serializers.Serializer):
+ model = User
+
+ """
+ Serializer for password change endpoint.
+ """
+ old_password = serializers.CharField(required=True)
+ new_password = serializers.CharField(required=True)
+ confirm_password = serializers.CharField(required=True)
+
+ def validate(self, data):
+ if data.get("old_password") == data.get("new_password"):
+ raise serializers.ValidationError(
+ {"error": "New password cannot be same as old password."}
+ )
+
+ if data.get("new_password") != data.get("confirm_password"):
+ raise serializers.ValidationError(
+ {"error": "Confirm password should be same as the new password."}
+ )
+
+ return data
+
+
+class ResetPasswordSerializer(serializers.Serializer):
+ model = User
+
+ """
+ Serializer for password change endpoint.
+ """
+ new_password = serializers.CharField(required=True)
diff --git a/apiserver/plane/api/serializers/view.py b/apiserver/plane/app/serializers/view.py
similarity index 96%
rename from apiserver/plane/api/serializers/view.py
rename to apiserver/plane/app/serializers/view.py
index a3b6f48be3..e7502609a7 100644
--- a/apiserver/plane/api/serializers/view.py
+++ b/apiserver/plane/app/serializers/view.py
@@ -57,7 +57,7 @@ class IssueViewSerializer(BaseSerializer):
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
- validated_data["query"] = dict()
+ validated_data["query"] = {}
return IssueView.objects.create(**validated_data)
def update(self, instance, validated_data):
@@ -65,7 +65,7 @@ class IssueViewSerializer(BaseSerializer):
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
- validated_data["query"] = dict()
+ validated_data["query"] = {}
validated_data["query"] = issue_filters(query_params, "PATCH")
return super().update(instance, validated_data)
diff --git a/apiserver/plane/app/serializers/webhook.py b/apiserver/plane/app/serializers/webhook.py
new file mode 100644
index 0000000000..961466d285
--- /dev/null
+++ b/apiserver/plane/app/serializers/webhook.py
@@ -0,0 +1,106 @@
+# Python imports
+import urllib
+import socket
+import ipaddress
+from urllib.parse import urlparse
+
+# Third party imports
+from rest_framework import serializers
+
+# Module imports
+from .base import DynamicBaseSerializer
+from plane.db.models import Webhook, WebhookLog
+from plane.db.models.webhook import validate_domain, validate_schema
+
+class WebhookSerializer(DynamicBaseSerializer):
+ url = serializers.URLField(validators=[validate_schema, validate_domain])
+
+ def create(self, validated_data):
+ url = validated_data.get("url", None)
+
+ # Extract the hostname from the URL
+ hostname = urlparse(url).hostname
+ if not hostname:
+ raise serializers.ValidationError({"url": "Invalid URL: No hostname found."})
+
+ # Resolve the hostname to IP addresses
+ try:
+ ip_addresses = socket.getaddrinfo(hostname, None)
+ except socket.gaierror:
+ raise serializers.ValidationError({"url": "Hostname could not be resolved."})
+
+ if not ip_addresses:
+ raise serializers.ValidationError({"url": "No IP addresses found for the hostname."})
+
+ for addr in ip_addresses:
+ ip = ipaddress.ip_address(addr[4][0])
+ if ip.is_private or ip.is_loopback:
+ raise serializers.ValidationError({"url": "URL resolves to a blocked IP address."})
+
+ # Additional validation for multiple request domains and their subdomains
+ request = self.context.get('request')
+ disallowed_domains = ['plane.so',] # Add your disallowed domains here
+ if request:
+ request_host = request.get_host().split(':')[0] # Remove port if present
+ disallowed_domains.append(request_host)
+
+ # Check if hostname is a subdomain or exact match of any disallowed domain
+ if any(hostname == domain or hostname.endswith('.' + domain) for domain in disallowed_domains):
+ raise serializers.ValidationError({"url": "URL domain or its subdomain is not allowed."})
+
+ return Webhook.objects.create(**validated_data)
+
+ def update(self, instance, validated_data):
+ url = validated_data.get("url", None)
+ if url:
+ # Extract the hostname from the URL
+ hostname = urlparse(url).hostname
+ if not hostname:
+ raise serializers.ValidationError({"url": "Invalid URL: No hostname found."})
+
+ # Resolve the hostname to IP addresses
+ try:
+ ip_addresses = socket.getaddrinfo(hostname, None)
+ except socket.gaierror:
+ raise serializers.ValidationError({"url": "Hostname could not be resolved."})
+
+ if not ip_addresses:
+ raise serializers.ValidationError({"url": "No IP addresses found for the hostname."})
+
+ for addr in ip_addresses:
+ ip = ipaddress.ip_address(addr[4][0])
+ if ip.is_private or ip.is_loopback:
+ raise serializers.ValidationError({"url": "URL resolves to a blocked IP address."})
+
+ # Additional validation for multiple request domains and their subdomains
+ request = self.context.get('request')
+ disallowed_domains = ['plane.so',] # Add your disallowed domains here
+ if request:
+ request_host = request.get_host().split(':')[0] # Remove port if present
+ disallowed_domains.append(request_host)
+
+ # Check if hostname is a subdomain or exact match of any disallowed domain
+ if any(hostname == domain or hostname.endswith('.' + domain) for domain in disallowed_domains):
+ raise serializers.ValidationError({"url": "URL domain or its subdomain is not allowed."})
+
+ return super().update(instance, validated_data)
+
+ class Meta:
+ model = Webhook
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ "secret_key",
+ ]
+
+
+class WebhookLogSerializer(DynamicBaseSerializer):
+
+ class Meta:
+ model = WebhookLog
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ "webhook"
+ ]
+
diff --git a/apiserver/plane/app/serializers/workspace.py b/apiserver/plane/app/serializers/workspace.py
new file mode 100644
index 0000000000..48a4bc44e3
--- /dev/null
+++ b/apiserver/plane/app/serializers/workspace.py
@@ -0,0 +1,153 @@
+# Third party imports
+from rest_framework import serializers
+
+# Module imports
+from .base import BaseSerializer
+from .user import UserLiteSerializer, UserAdminLiteSerializer
+
+from plane.db.models import (
+ User,
+ Workspace,
+ WorkspaceMember,
+ Team,
+ TeamMember,
+ WorkspaceMemberInvite,
+ WorkspaceTheme,
+)
+
+
+class WorkSpaceSerializer(BaseSerializer):
+ owner = UserLiteSerializer(read_only=True)
+ total_members = serializers.IntegerField(read_only=True)
+ total_issues = serializers.IntegerField(read_only=True)
+
+ def validated(self, data):
+ if data.get("slug") in [
+ "404",
+ "accounts",
+ "api",
+ "create-workspace",
+ "god-mode",
+ "installations",
+ "invitations",
+ "onboarding",
+ "profile",
+ "spaces",
+ "workspace-invitations",
+ "password",
+ ]:
+ raise serializers.ValidationError({"slug": "Slug is not valid"})
+
+ class Meta:
+ model = Workspace
+ fields = "__all__"
+ read_only_fields = [
+ "id",
+ "created_by",
+ "updated_by",
+ "created_at",
+ "updated_at",
+ "owner",
+ ]
+
+class WorkspaceLiteSerializer(BaseSerializer):
+ class Meta:
+ model = Workspace
+ fields = [
+ "name",
+ "slug",
+ "id",
+ ]
+ read_only_fields = fields
+
+
+
+class WorkSpaceMemberSerializer(BaseSerializer):
+ member = UserLiteSerializer(read_only=True)
+ workspace = WorkspaceLiteSerializer(read_only=True)
+
+ class Meta:
+ model = WorkspaceMember
+ fields = "__all__"
+
+
+class WorkspaceMemberMeSerializer(BaseSerializer):
+
+ class Meta:
+ model = WorkspaceMember
+ fields = "__all__"
+
+
+class WorkspaceMemberAdminSerializer(BaseSerializer):
+ member = UserAdminLiteSerializer(read_only=True)
+ workspace = WorkspaceLiteSerializer(read_only=True)
+
+ class Meta:
+ model = WorkspaceMember
+ fields = "__all__"
+
+
+class WorkSpaceMemberInviteSerializer(BaseSerializer):
+ workspace = WorkSpaceSerializer(read_only=True)
+ total_members = serializers.IntegerField(read_only=True)
+ created_by_detail = UserLiteSerializer(read_only=True, source="created_by")
+
+ class Meta:
+ model = WorkspaceMemberInvite
+ fields = "__all__"
+
+
+class TeamSerializer(BaseSerializer):
+ members_detail = UserLiteSerializer(read_only=True, source="members", many=True)
+ members = serializers.ListField(
+ child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
+ write_only=True,
+ required=False,
+ )
+
+ class Meta:
+ model = Team
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ "created_by",
+ "updated_by",
+ "created_at",
+ "updated_at",
+ ]
+
+ def create(self, validated_data, **kwargs):
+ if "members" in validated_data:
+ members = validated_data.pop("members")
+ workspace = self.context["workspace"]
+ team = Team.objects.create(**validated_data, workspace=workspace)
+ team_members = [
+ TeamMember(member=member, team=team, workspace=workspace)
+ for member in members
+ ]
+ TeamMember.objects.bulk_create(team_members, batch_size=10)
+ return team
+ team = Team.objects.create(**validated_data)
+ return team
+
+ def update(self, instance, validated_data):
+ if "members" in validated_data:
+ members = validated_data.pop("members")
+ TeamMember.objects.filter(team=instance).delete()
+ team_members = [
+ TeamMember(member=member, team=instance, workspace=instance.workspace)
+ for member in members
+ ]
+ TeamMember.objects.bulk_create(team_members, batch_size=10)
+ return super().update(instance, validated_data)
+ return super().update(instance, validated_data)
+
+
+class WorkspaceThemeSerializer(BaseSerializer):
+ class Meta:
+ model = WorkspaceTheme
+ fields = "__all__"
+ read_only_fields = [
+ "workspace",
+ "actor",
+ ]
diff --git a/apiserver/plane/app/urls/__init__.py b/apiserver/plane/app/urls/__init__.py
new file mode 100644
index 0000000000..d8334ed570
--- /dev/null
+++ b/apiserver/plane/app/urls/__init__.py
@@ -0,0 +1,48 @@
+from .analytic import urlpatterns as analytic_urls
+from .asset import urlpatterns as asset_urls
+from .authentication import urlpatterns as authentication_urls
+from .config import urlpatterns as configuration_urls
+from .cycle import urlpatterns as cycle_urls
+from .estimate import urlpatterns as estimate_urls
+from .external import urlpatterns as external_urls
+from .importer import urlpatterns as importer_urls
+from .inbox import urlpatterns as inbox_urls
+from .integration import urlpatterns as integration_urls
+from .issue import urlpatterns as issue_urls
+from .module import urlpatterns as module_urls
+from .notification import urlpatterns as notification_urls
+from .page import urlpatterns as page_urls
+from .project import urlpatterns as project_urls
+from .search import urlpatterns as search_urls
+from .state import urlpatterns as state_urls
+from .user import urlpatterns as user_urls
+from .views import urlpatterns as view_urls
+from .workspace import urlpatterns as workspace_urls
+from .api import urlpatterns as api_urls
+from .webhook import urlpatterns as webhook_urls
+
+
+urlpatterns = [
+ *analytic_urls,
+ *asset_urls,
+ *authentication_urls,
+ *configuration_urls,
+ *cycle_urls,
+ *estimate_urls,
+ *external_urls,
+ *importer_urls,
+ *inbox_urls,
+ *integration_urls,
+ *issue_urls,
+ *module_urls,
+ *notification_urls,
+ *page_urls,
+ *project_urls,
+ *search_urls,
+ *state_urls,
+ *user_urls,
+ *view_urls,
+ *workspace_urls,
+ *api_urls,
+ *webhook_urls,
+]
\ No newline at end of file
diff --git a/apiserver/plane/app/urls/analytic.py b/apiserver/plane/app/urls/analytic.py
new file mode 100644
index 0000000000..668268350b
--- /dev/null
+++ b/apiserver/plane/app/urls/analytic.py
@@ -0,0 +1,46 @@
+from django.urls import path
+
+
+from plane.app.views import (
+ AnalyticsEndpoint,
+ AnalyticViewViewset,
+ SavedAnalyticEndpoint,
+ ExportAnalyticsEndpoint,
+ DefaultAnalyticsEndpoint,
+)
+
+
+urlpatterns = [
+ path(
+ "workspaces//analytics/",
+ AnalyticsEndpoint.as_view(),
+ name="plane-analytics",
+ ),
+ path(
+ "workspaces//analytic-view/",
+ AnalyticViewViewset.as_view({"get": "list", "post": "create"}),
+ name="analytic-view",
+ ),
+ path(
+ "workspaces//analytic-view//",
+ AnalyticViewViewset.as_view(
+ {"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
+ ),
+ name="analytic-view",
+ ),
+ path(
+ "workspaces//saved-analytic-view//",
+ SavedAnalyticEndpoint.as_view(),
+ name="saved-analytic-view",
+ ),
+ path(
+ "workspaces//export-analytics/",
+ ExportAnalyticsEndpoint.as_view(),
+ name="export-analytics",
+ ),
+ path(
+ "workspaces//default-analytics/",
+ DefaultAnalyticsEndpoint.as_view(),
+ name="default-analytics",
+ ),
+]
diff --git a/apiserver/plane/app/urls/api.py b/apiserver/plane/app/urls/api.py
new file mode 100644
index 0000000000..b77ea85300
--- /dev/null
+++ b/apiserver/plane/app/urls/api.py
@@ -0,0 +1,17 @@
+from django.urls import path
+from plane.app.views import ApiTokenEndpoint
+
+urlpatterns = [
+ # API Tokens
+ path(
+ "workspaces//api-tokens/",
+ ApiTokenEndpoint.as_view(),
+ name="api-tokens",
+ ),
+ path(
+ "workspaces//api-tokens//",
+ ApiTokenEndpoint.as_view(),
+ name="api-tokens",
+ ),
+ ## End API Tokens
+]
diff --git a/apiserver/plane/app/urls/asset.py b/apiserver/plane/app/urls/asset.py
new file mode 100644
index 0000000000..2d84b93e0b
--- /dev/null
+++ b/apiserver/plane/app/urls/asset.py
@@ -0,0 +1,41 @@
+from django.urls import path
+
+
+from plane.app.views import (
+ FileAssetEndpoint,
+ UserAssetsEndpoint,
+ FileAssetViewSet,
+)
+
+
+urlpatterns = [
+ path(
+ "workspaces//file-assets/",
+ FileAssetEndpoint.as_view(),
+ name="file-assets",
+ ),
+ path(
+ "workspaces/file-assets///",
+ FileAssetEndpoint.as_view(),
+ name="file-assets",
+ ),
+ path(
+ "users/file-assets/",
+ UserAssetsEndpoint.as_view(),
+ name="user-file-assets",
+ ),
+ path(
+ "users/file-assets//",
+ UserAssetsEndpoint.as_view(),
+ name="user-file-assets",
+ ),
+ path(
+ "workspaces/file-assets///restore/",
+ FileAssetViewSet.as_view(
+ {
+ "post": "restore",
+ }
+ ),
+ name="file-assets-restore",
+ ),
+]
diff --git a/apiserver/plane/app/urls/authentication.py b/apiserver/plane/app/urls/authentication.py
new file mode 100644
index 0000000000..ec3fa78edc
--- /dev/null
+++ b/apiserver/plane/app/urls/authentication.py
@@ -0,0 +1,55 @@
+from django.urls import path
+
+from rest_framework_simplejwt.views import TokenRefreshView
+
+
+from plane.app.views import (
+ # Authentication
+ SignInEndpoint,
+ SignOutEndpoint,
+ MagicSignInEndpoint,
+ OauthEndpoint,
+ EmailCheckEndpoint,
+ ## End Authentication
+ # Auth Extended
+ ForgotPasswordEndpoint,
+ ResetPasswordEndpoint,
+ ChangePasswordEndpoint,
+ ## End Auth Extender
+ # API Tokens
+ ApiTokenEndpoint,
+ ## End API Tokens
+)
+
+
+urlpatterns = [
+ # Social Auth
+ path("email-check/", EmailCheckEndpoint.as_view(), name="email"),
+ path("social-auth/", OauthEndpoint.as_view(), name="oauth"),
+ # Auth
+ path("sign-in/", SignInEndpoint.as_view(), name="sign-in"),
+ path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"),
+ # magic sign in
+ path("magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"),
+ path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
+ # Password Manipulation
+ path(
+ "users/me/change-password/",
+ ChangePasswordEndpoint.as_view(),
+ name="change-password",
+ ),
+ path(
+ "reset-password///",
+ ResetPasswordEndpoint.as_view(),
+ name="password-reset",
+ ),
+ path(
+ "forgot-password/",
+ ForgotPasswordEndpoint.as_view(),
+ name="forgot-password",
+ ),
+ # API Tokens
+ path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"),
+ path("api-tokens//", ApiTokenEndpoint.as_view(), name="api-tokens"),
+ ## End API Tokens
+]
diff --git a/apiserver/plane/app/urls/config.py b/apiserver/plane/app/urls/config.py
new file mode 100644
index 0000000000..12beb63aad
--- /dev/null
+++ b/apiserver/plane/app/urls/config.py
@@ -0,0 +1,12 @@
+from django.urls import path
+
+
+from plane.app.views import ConfigurationEndpoint
+
+urlpatterns = [
+ path(
+ "configs/",
+ ConfigurationEndpoint.as_view(),
+ name="configuration",
+ ),
+]
\ No newline at end of file
diff --git a/apiserver/plane/app/urls/cycle.py b/apiserver/plane/app/urls/cycle.py
new file mode 100644
index 0000000000..46e6a5e847
--- /dev/null
+++ b/apiserver/plane/app/urls/cycle.py
@@ -0,0 +1,87 @@
+from django.urls import path
+
+
+from plane.app.views import (
+ CycleViewSet,
+ CycleIssueViewSet,
+ CycleDateCheckEndpoint,
+ CycleFavoriteViewSet,
+ TransferCycleIssueEndpoint,
+)
+
+
+urlpatterns = [
+ path(
+ "workspaces//projects//cycles/",
+ CycleViewSet.as_view(
+ {
+ "get": "list",
+ "post": "create",
+ }
+ ),
+ name="project-cycle",
+ ),
+ path(
+ "workspaces//projects//cycles//",
+ CycleViewSet.as_view(
+ {
+ "get": "retrieve",
+ "put": "update",
+ "patch": "partial_update",
+ "delete": "destroy",
+ }
+ ),
+ name="project-cycle",
+ ),
+ path(
+ "workspaces//projects//cycles//cycle-issues/",
+ CycleIssueViewSet.as_view(
+ {
+ "get": "list",
+ "post": "create",
+ }
+ ),
+ name="project-issue-cycle",
+ ),
+ path(
+ "workspaces//projects//cycles//cycle-issues//",
+ CycleIssueViewSet.as_view(
+ {
+ "get": "retrieve",
+ "put": "update",
+ "patch": "partial_update",
+ "delete": "destroy",
+ }
+ ),
+ name="project-issue-cycle",
+ ),
+ path(
+ "workspaces//projects//cycles/date-check/",
+ CycleDateCheckEndpoint.as_view(),
+ name="project-cycle-date",
+ ),
+ path(
+ "workspaces//projects//user-favorite-cycles/",
+ CycleFavoriteViewSet.as_view(
+ {
+ "get": "list",
+ "post": "create",
+ }
+ ),
+ name="user-favorite-cycle",
+ ),
+ path(
+ "workspaces//projects//user-favorite-cycles//",
+ CycleFavoriteViewSet.as_view(
+ {
+ "delete": "destroy",
+ }
+ ),
+ name="user-favorite-cycle",
+ ),
+ path(
+ "workspaces//projects//cycles//transfer-issues/",
+ TransferCycleIssueEndpoint.as_view(),
+ name="transfer-issues",
+ ),
+]
diff --git a/apiserver/plane/app/urls/estimate.py b/apiserver/plane/app/urls/estimate.py
new file mode 100644
index 0000000000..d8571ff0c9
--- /dev/null
+++ b/apiserver/plane/app/urls/estimate.py
@@ -0,0 +1,37 @@
+from django.urls import path
+
+
+from plane.app.views import (
+ ProjectEstimatePointEndpoint,
+ BulkEstimatePointEndpoint,
+)
+
+
+urlpatterns = [
+ path(
+ "workspaces//projects//project-estimates/",
+ ProjectEstimatePointEndpoint.as_view(),
+ name="project-estimate-points",
+ ),
+ path(
+ "workspaces//projects//estimates/",
+ BulkEstimatePointEndpoint.as_view(
+ {
+ "get": "list",
+ "post": "create",
+ }
+ ),
+ name="bulk-create-estimate-points",
+ ),
+ path(
+ "workspaces//projects//estimates//",
+ BulkEstimatePointEndpoint.as_view(
+ {
+ "get": "retrieve",
+ "patch": "partial_update",
+ "delete": "destroy",
+ }
+ ),
+ name="bulk-create-estimate-points",
+ ),
+]
diff --git a/apiserver/plane/app/urls/external.py b/apiserver/plane/app/urls/external.py
new file mode 100644
index 0000000000..774e6fb7cd
--- /dev/null
+++ b/apiserver/plane/app/urls/external.py
@@ -0,0 +1,25 @@
+from django.urls import path
+
+
+from plane.app.views import UnsplashEndpoint
+from plane.app.views import ReleaseNotesEndpoint
+from plane.app.views import GPTIntegrationEndpoint
+
+
+urlpatterns = [
+ path(
+ "unsplash/",
+ UnsplashEndpoint.as_view(),
+ name="unsplash",
+ ),
+ path(
+ "release-notes/",
+ ReleaseNotesEndpoint.as_view(),
+ name="release-notes",
+ ),
+ path(
+ "workspaces//projects//ai-assistant/",
+ GPTIntegrationEndpoint.as_view(),
+ name="importer",
+ ),
+]
diff --git a/apiserver/plane/app/urls/importer.py b/apiserver/plane/app/urls/importer.py
new file mode 100644
index 0000000000..f3a018d789
--- /dev/null
+++ b/apiserver/plane/app/urls/importer.py
@@ -0,0 +1,37 @@
+from django.urls import path
+
+
+from plane.app.views import (
+ ServiceIssueImportSummaryEndpoint,
+ ImportServiceEndpoint,
+ UpdateServiceImportStatusEndpoint,
+)
+
+
+urlpatterns = [
+ path(
+ "workspaces//importers//",
+ ServiceIssueImportSummaryEndpoint.as_view(),
+ name="importer-summary",
+ ),
+ path(
+ "workspaces//projects/importers//",
+ ImportServiceEndpoint.as_view(),
+ name="importer",
+ ),
+ path(
+ "workspaces//importers/",
+ ImportServiceEndpoint.as_view(),
+ name="importer",
+ ),
+ path(
+ "workspaces//importers///",
+ ImportServiceEndpoint.as_view(),
+ name="importer",
+ ),
+ path(
+ "workspaces//projects//service//importers//",
+ UpdateServiceImportStatusEndpoint.as_view(),
+ name="importer-status",
+ ),
+]
diff --git a/apiserver/plane/app/urls/inbox.py b/apiserver/plane/app/urls/inbox.py
new file mode 100644
index 0000000000..16ea40b21a
--- /dev/null
+++ b/apiserver/plane/app/urls/inbox.py
@@ -0,0 +1,53 @@
+from django.urls import path
+
+
+from plane.app.views import (
+ InboxViewSet,
+ InboxIssueViewSet,
+)
+
+
+urlpatterns = [
+ path(
+ "workspaces//projects//inboxes/",
+ InboxViewSet.as_view(
+ {
+ "get": "list",
+ "post": "create",
+ }
+ ),
+ name="inbox",
+ ),
+ path(
+ "workspaces//projects//inboxes//",
+ InboxViewSet.as_view(
+ {
+ "get": "retrieve",
+ "patch": "partial_update",
+ "delete": "destroy",
+ }
+ ),
+ name="inbox",
+ ),
+ path(
+ "workspaces//projects//inboxes//inbox-issues/",
+ InboxIssueViewSet.as_view(
+ {
+ "get": "list",
+ "post": "create",
+ }
+ ),
+ name="inbox-issue",
+ ),
+ path(
+ "workspaces//projects//inboxes//inbox-issues//",
+ InboxIssueViewSet.as_view(
+ {
+ "get": "retrieve",
+ "patch": "partial_update",
+ "delete": "destroy",
+ }
+ ),
+ name="inbox-issue",
+ ),
+]
diff --git a/apiserver/plane/app/urls/integration.py b/apiserver/plane/app/urls/integration.py
new file mode 100644
index 0000000000..cf3f82d5a4
--- /dev/null
+++ b/apiserver/plane/app/urls/integration.py
@@ -0,0 +1,150 @@
+from django.urls import path
+
+
+from plane.app.views import (
+ IntegrationViewSet,
+ WorkspaceIntegrationViewSet,
+ GithubRepositoriesEndpoint,
+ GithubRepositorySyncViewSet,
+ GithubIssueSyncViewSet,
+ GithubCommentSyncViewSet,
+ BulkCreateGithubIssueSyncEndpoint,
+ SlackProjectSyncViewSet,
+)
+
+
+urlpatterns = [
+ path(
+ "integrations/",
+ IntegrationViewSet.as_view(
+ {
+ "get": "list",
+ "post": "create",
+ }
+ ),
+ name="integrations",
+ ),
+ path(
+ "integrations//",
+ IntegrationViewSet.as_view(
+ {
+ "get": "retrieve",
+ "patch": "partial_update",
+ "delete": "destroy",
+ }
+ ),
+ name="integrations",
+ ),
+ path(
+ "workspaces//workspace-integrations/",
+ WorkspaceIntegrationViewSet.as_view(
+ {
+ "get": "list",
+ }
+ ),
+ name="workspace-integrations",
+ ),
+ path(
+ "workspaces//workspace-integrations//",
+ WorkspaceIntegrationViewSet.as_view(
+ {
+ "post": "create",
+ }
+ ),
+ name="workspace-integrations",
+ ),
+ path(
+ "workspaces//workspace-integrations//provider/",
+ WorkspaceIntegrationViewSet.as_view(
+ {
+ "get": "retrieve",
+ "delete": "destroy",
+ }
+ ),
+ name="workspace-integrations",
+ ),
+ # Github Integrations
+ path(
+ "workspaces//workspace-integrations//github-repositories/",
+ GithubRepositoriesEndpoint.as_view(),
+ ),
+ path(
+ "workspaces//projects//workspace-integrations//github-repository-sync/",
+ GithubRepositorySyncViewSet.as_view(
+ {
+ "get": "list",
+ "post": "create",
+ }
+ ),
+ ),
+ path(
+ "workspaces//projects//workspace-integrations//github-repository-sync//",
+ GithubRepositorySyncViewSet.as_view(
+ {
+ "get": "retrieve",
+ "delete": "destroy",
+ }
+ ),
+ ),
+ path(
+ "workspaces//projects//github-repository-sync//github-issue-sync/",
+ GithubIssueSyncViewSet.as_view(
+ {
+ "post": "create",
+ "get": "list",
+ }
+ ),
+ ),
+ path(
+ "workspaces//projects//github-repository-sync//bulk-create-github-issue-sync/",
+ BulkCreateGithubIssueSyncEndpoint.as_view(),
+ ),
+ path(
+ "workspaces//projects//github-repository-sync//github-issue-sync//",
+ GithubIssueSyncViewSet.as_view(
+ {
+ "get": "retrieve",
+ "delete": "destroy",
+ }
+ ),
+ ),
+ path(
+ "workspaces//projects//github-repository-sync//github-issue-sync//github-comment-sync/",
+ GithubCommentSyncViewSet.as_view(
+ {
+ "post": "create",
+ "get": "list",
+ }
+ ),
+ ),
+ path(
+ "workspaces//projects//github-repository-sync//github-issue-sync//github-comment-sync//",
+ GithubCommentSyncViewSet.as_view(
+ {
+ "get": "retrieve",
+ "delete": "destroy",
+ }
+ ),
+ ),
+ ## End Github Integrations
+ # Slack Integration
+ path(
+ "workspaces//projects//workspace-integrations//project-slack-sync/",
+ SlackProjectSyncViewSet.as_view(
+ {
+ "post": "create",
+ "get": "list",
+ }
+ ),
+ ),
+ path(
+ "workspaces//projects//workspace-integrations//project-slack-sync//",
+ SlackProjectSyncViewSet.as_view(
+ {
+ "delete": "destroy",
+ "get": "retrieve",
+ }
+ ),
+ ),
+ ## End Slack Integration
+]
diff --git a/apiserver/plane/app/urls/issue.py b/apiserver/plane/app/urls/issue.py
new file mode 100644
index 0000000000..971fbc395d
--- /dev/null
+++ b/apiserver/plane/app/urls/issue.py
@@ -0,0 +1,315 @@
+from django.urls import path
+
+
+from plane.app.views import (
+ IssueViewSet,
+ LabelViewSet,
+ BulkCreateIssueLabelsEndpoint,
+ BulkDeleteIssuesEndpoint,
+ BulkImportIssuesEndpoint,
+ UserWorkSpaceIssues,
+ SubIssuesEndpoint,
+ IssueLinkViewSet,
+ IssueAttachmentEndpoint,
+ ExportIssuesEndpoint,
+ IssueActivityEndpoint,
+ IssueCommentViewSet,
+ IssueSubscriberViewSet,
+ IssueReactionViewSet,
+ CommentReactionViewSet,
+ IssueUserDisplayPropertyEndpoint,
+ IssueArchiveViewSet,
+ IssueRelationViewSet,
+ IssueDraftViewSet,
+)
+
+
+urlpatterns = [
+ path(
+ "workspaces//projects//issues/",
+ IssueViewSet.as_view(
+ {
+ "get": "list",
+ "post": "create",
+ }
+ ),
+ name="project-issue",
+ ),
+ path(
+ "workspaces//projects//issues//",
+ IssueViewSet.as_view(
+ {
+ "get": "retrieve",
+ "put": "update",
+ "patch": "partial_update",
+ "delete": "destroy",
+ }
+ ),
+ name="project-issue",
+ ),
+ path(
+ "workspaces//projects//issue-labels/",
+ LabelViewSet.as_view(
+ {
+ "get": "list",
+ "post": "create",
+ }
+ ),
+ name="project-issue-labels",
+ ),
+ path(
+ "workspaces//projects//issue-labels//",
+ LabelViewSet.as_view(
+ {
+ "get": "retrieve",
+ "put": "update",
+ "patch": "partial_update",
+ "delete": "destroy",
+ }
+ ),
+ name="project-issue-labels",
+ ),
+ path(
+ "workspaces//projects//bulk-create-labels/",
+ BulkCreateIssueLabelsEndpoint.as_view(),
+ name="project-bulk-labels",
+ ),
+ path(
+ "workspaces//projects//bulk-delete-issues/",
+ BulkDeleteIssuesEndpoint.as_view(),
+ name="project-issues-bulk",
+ ),
+ path(
+ "workspaces//projects//bulk-import-issues//",
+ BulkImportIssuesEndpoint.as_view(),
+ name="project-issues-bulk",
+ ),
+ path(
+ "workspaces//my-issues/",
+ UserWorkSpaceIssues.as_view(),
+ name="workspace-issues",
+ ),
+ path(
+ "workspaces//projects//issues//sub-issues/",
+ SubIssuesEndpoint.as_view(),
+ name="sub-issues",
+ ),
+ path(
+ "workspaces//projects//issues//issue-links/",
+ IssueLinkViewSet.as_view(
+ {
+ "get": "list",
+ "post": "create",
+ }
+ ),
+ name="project-issue-links",
+ ),
+ path(
+ "workspaces//projects//issues//issue-links//",
+ IssueLinkViewSet.as_view(
+ {
+ "get": "retrieve",
+ "put": "update",
+ "patch": "partial_update",
+ "delete": "destroy",
+ }
+ ),
+ name="project-issue-links",
+ ),
+ path(
+ "workspaces//projects//issues//issue-attachments/",
+ IssueAttachmentEndpoint.as_view(),
+ name="project-issue-attachments",
+ ),
+ path(
+ "workspaces//projects//issues//issue-attachments//",
+ IssueAttachmentEndpoint.as_view(),
+ name="project-issue-attachments",
+ ),
+ path(
+ "workspaces//export-issues/",
+ ExportIssuesEndpoint.as_view(),
+ name="export-issues",
+ ),
+ ## End Issues
+ ## Issue Activity
+ path(
+ "workspaces//projects//issues//history/",
+ IssueActivityEndpoint.as_view(),
+ name="project-issue-history",
+ ),
+ ## Issue Activity
+ ## IssueComments
+ path(
+ "workspaces//projects//issues//comments/",
+ IssueCommentViewSet.as_view(
+ {
+ "get": "list",
+ "post": "create",
+ }
+ ),
+ name="project-issue-comment",
+ ),
+ path(
+ "workspaces//projects//issues//comments//",
+ IssueCommentViewSet.as_view(
+ {
+ "get": "retrieve",
+ "put": "update",
+ "patch": "partial_update",
+ "delete": "destroy",
+ }
+ ),
+ name="project-issue-comment",
+ ),
+ ## End IssueComments
+ # Issue Subscribers
+ path(
+ "workspaces//projects//issues//issue-subscribers/",
+ IssueSubscriberViewSet.as_view(
+ {
+ "get": "list",
+ "post": "create",
+ }
+ ),
+ name="project-issue-subscribers",
+ ),
+ path(
+ "workspaces//projects//issues//issue-subscribers//",
+ IssueSubscriberViewSet.as_view({"delete": "destroy"}),
+ name="project-issue-subscribers",
+ ),
+ path(
+ "workspaces//projects//issues//subscribe/",
+ IssueSubscriberViewSet.as_view(
+ {
+ "get": "subscription_status",
+ "post": "subscribe",
+ "delete": "unsubscribe",
+ }
+ ),
+ name="project-issue-subscribers",
+ ),
+ ## End Issue Subscribers
+ # Issue Reactions
+ path(
+ "workspaces//projects//issues//reactions/",
+ IssueReactionViewSet.as_view(
+ {
+ "get": "list",
+ "post": "create",
+ }
+ ),
+ name="project-issue-reactions",
+ ),
+ path(
+ "workspaces//projects//issues//reactions/