Compare commits

..
Author SHA1 Message Date
NarayanBavisetti aa0aeefbbe fix: draft issue title fixes 2024-01-03 12:29:53 +05:30
NarayanBavisetti 63de81c2be Merge branch 'develop' of github.com:makeplane/plane into fix/issue-state-change 2024-01-03 11:59:50 +05:30
NarayanBavisetti aca1546f69 fix: untitled draft issue 2024-01-02 19:11:25 +05:30
Bavisetti NarayanandGitHub 87cd44bcd2 fix: migration file fixes (#3302) 2024-01-02 18:52:46 +05:30
+7 804b7d8663 refactor: MobX store structure (#3228)
* query params from router as computed

* chore: setup workspace store and sub-stores

* chore: update router query store

* chore: update store types

* fix: pages store changes

* change observables and retain object reference

* fix build errors

* chore: changed the structure of workspace, project, cycle, module and pages

* fix: pages fixes

* fix: merge conflicts resolved

* chore: fixed workspace list

* chore: update workspace store accroding to the new response

* fix: adding page details to store

* fix: adding new contexts and providers

* dev: issues store and filters in new store

* dev: optimised the issue fetching in issue base store

* chore: project views id mapped

* update lodash set to directly run inside runInaction since it mutates the object

* fix: context changes

* code refactor kanban for better mainatinability

* optimize Kanban for performance

* chore: implemented hooks for all the created stores

* chore: removed bridge id

* css change and refactor

* chore: update cycle store structure

* chore: implement the new label root store

* chore: removed object structure

* chore: implement project view hook

* Kanban new store implementation for project issues

* fix project root for kanban

* feat: workspace and project members endpoint (#3092)

* fix: merge conflicts resolved

* issue properties optimization

* chore: user stores

* chore: create new store context and update hooks

* chore: setup inbox store and implement router store

* chore: initialize and implement project estimate store

* chore: initialize global view store

* kanban and list view optimization

* chore: use new cycle and module store. (#3172)

* chore: use new cycle and module store.

* chore: minor improvements.

* Revert "chore: merge develop"

This reverts commit 9d2e0e29e7, reversing
changes made to 9595493c42.

* chore: implement useGlobalView hook

* refactor: projects & inbox store instances (#3179)

* refactor: projects & inbox store instances

* fix: formatting

* fix: action usage

* chore: implement useProjectState hook. (#3185)

* dev: issue, cycle store optimiation

* fix build for code

* dev: removed dummy variables

* dev: issue store

* fix: adding todos

* chore: removing legacy store

* dev: issues store types and typos

* chore: cycle module user properties

* fix legacy store deletion issues

* chore: change POST to PATCH

* fix issues rendering for project root

* chore: removed workspace details in workpsaceinvite

* chore: created models for display properties

* chore: setup member store and implement it everywhere

* refactor: module store (#3202)

* refactor: cycle store (#3192)

* refator: cycle store

* some more improvements.

* chore: implement useLabel hook. (#3190)

* refactor: inbox & project related stores. (#3193)

* refactor: inbox -> filter, issues, inoxes & project -> publish, projects store

* refactor: workspace-project-id name

* fix kanban dropdown overlapping issue

* fix kanban layout minor re rendering

* chore: implement useMember store everywhere

* chore: create and implement editor mention store

* chore: removed the issue view user property

* chore: created at id changed

* dev: segway intgegration (#3132)

* feat: implemented rabbitmq

* dev: initialize segway with queue setup

* dev: import refactors

* dev: create communication with the segway server

* dev: create new workers

* dev: create celery node queue for consuming messages from django

* dev: node to celery connection

* dev: setup segway and django connection

* dev: refactor the structure and add database integration to the app

* dev: add external id and source added

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>

* dev: github importer (#3205)

* dev: initiate github import

* dev: github importer all issues import

* dev: github comments and links for the imported issues

* dev: update controller to use logger and spread the resultData in getAllEntities

* dev: removed console log

* dev: update code structure and sync functions

* dev: updated retry logic when exception

* dev: add imported data as well

* dev: update logger and repo fetch

* dev: update jira integration to new structure

* dev: update migrations

* dev: update the reason field

* chore: workspace object id removed

* chore: view's creation fixed

* refactor: mobx store improvements. (#3213)

* fix: state and label errors

* chore: remove legacy code

* fix: branch build fix (#3214)

* branch build fix for release-* in case of space,backend,proxy

* fixes

* chore: update store names and types

* fix - file size limit not work on plane.settings.production (#3160)

* fix - file size limit not work on plane.settings.production

* fix - file size limit not work on plane.settings.production

* fix - file size limit not work on plane.settings.production, move to common.py

---------

Co-authored-by: luanduongtel4vn <hoangluan@tel4vn.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>

* style: instance admin email settings ui & ux update. (#3186)

* refactor: use-user-auth hook (#3215)

* refactor: use-user-auth hook

* fix: user store currentUserLoader

* refactor: project-view & application related stores (#3207)

* refactor: project-view & application related stores

* rename: projectViews -> projectViewIds

* fix: project-view favourite state in store

* chore: remove unnecessary hooks and contexts (#3217)

* chore: update issue assignee property component

* chore: bug fixes & improvement (#3218)

* chore: draft issue validation added to prevent saving empty or whitespace title

* chore: resolve scrolling issue in page empty state

* chore: kanban layout quick add issue improvement

* fix: bugs & improvements (#3189)

* fix: workspace invitation modal form values reset

* fix: profile sidebar avatar letter

* [refactor] Editor code refactoring (#3194)

* removed relative imports from editor core

* Update issue widget file paths and imports to use kebab case instead of camel case, to align with coding conventions and improve consistency.

* Update Tiptap core and extensions versions to 2.1.13 and Tiptap React version to 2.1.13. Update Tiptap table imports to use the new location in package @tiptap/pm/tables. Update AlertLabel component to use the new type definition for LucideIcon.

* updated lock file

* removed default exports from editor/core

* fixed injecting css into the core package itself

* seperated css code to have single source of origin wrt to the package

* removed default imports from document editor

* all instances using index as key while mapping fixed

* Update Lite Text Editor package.json to remove @plane/editor-types as a dependency.

Update Lite Text Editor index.ts to update the import of IMentionSuggestion and IMentionHighlight from @plane/editor-types to @plane/editor-core.

Update Lite Text Editor ui/index.tsx to update the import of UploadImage, DeleteImage, IMentionSuggestion, and RestoreImage from @plane/editor-types to @plane/editor-core.

Update Lite Text Editor ui/menus/fixed-menu/index.tsx to update the import of UploadImage from @plane/editor-types to @plane/editor-core.

Update turbo.json to remove @plane/editor-types#build as a dependency for @plane/lite-text-editor#build, @plane/rich-text-editor#build, and @plane/document-editor#build.

* Remove deprecated import and adjust tippy.js usage in the slash-commands.tsx file of the editor extensions package.

* Update dependencies in `rich-text-editor/package.json`, remove `@plane/editor-types` and add `@plane/editor-core` in `rich-text-editor/src/index.ts`, and update imports in `rich-text-editor/src/ui/extensions/index.tsx` and `rich-text-editor/src/ui/index.tsx` to use `@plane/editor-core` instead of `@plane/editor-types`.

* Update package.json dependencies and add new types for image deletion, upload, restore, mention highlight, mention suggestion, and slash command item.

* Update import statements in various files to use the new package "@plane/editor-core" instead of "@plane/editor-types".

* fixed document editor to follow conventions

* Refactor imports in the Rich Text Editor package to use relative paths instead of absolute paths.

- Updated imports in `index.ts`, `ui/index.tsx`, and `ui/menus/bubble-menu/index.tsx` to use relative paths.
- Updated `tsconfig.json` to include the `baseUrl` compiler option and adjust the `include` and `exclude` paths.

* Refactor Lite Text Editor code to use relative import paths instead of absolute import paths.

* Added LucideIconType to the exports in index.ts for use in other files.
Created a new file lucide-icon.ts which contains the type LucideIconType.
Updated the icon type in HeadingOneItem in menu-items/index.tsx to use LucideIconType.
Updated the Icon type in AlertLabel in alert-label.tsx to use LucideIconType.
Updated the Icon type in VerticalDropdownItemProps in vertical-dropdown-menu.tsx to use LucideIconType.
Updated the Icon type in BubbleMenuItem in fixed-menu/index.tsx to use LucideIconType.
Deleted the file tooltip.tsx since it is no longer used.
Updated the Icon type in BubbleMenuItem in bubble-menu/index.tsx to use LucideIconType.

* ♻️ refactor: simplify rendering logic in slash-commands.tsx

The rendering logic in the file "slash-commands.tsx" has been simplified. Previously, the code used inline positioning for the popup, but it has now been removed. Instead of appending the popup to the document body, it is now appended to the element with the ID "tiptap-container". The "flip" option has also been removed. These changes have improved the readability and maintainability of the code.

* fixed build errors caused due to core's internal imports

* regression: fixed pages not saving issue and not duplicating with proper content issue

* build: Update @tiptap dependencies

Updated the @tiptap dependencies in the package.json files of `document-editor`, `extensions`, and `rich-text-editor` packages to version 2.1.13.

* 🚑 fix: Correct appendTo selector in slash-commands.tsx

Update the `appendTo` function call in `slash-commands.tsx` to use the correct selector `#editor-container` instead of `#tiptap-container`. This ensures that the component is appended to the appropriate container in the editor extension.

Note: The commit message assumes that the change is a fix for an issue or error. If it's not a fix, please provide more context so that an appropriate commit type can be determined.

* style: email placeholder changed across the platform (#3206)

* style: email placeholder changed across the platform

* fix: placeholder text

* dev: updated new filter endpoints and restructured issue and issue filters store

* implement issues and replace useMobxStore

* remove all store legacy references

* dev: updated the orderby and subgroupby filters data

* dev:added projectId in issue filters for consistency

* fix more build errors

* dev: updated profile issues

* dev: removed store legacy

* dev: active cycle issues in the cycle issue store

* fix additional build errors and memoize issueActions in each layout component

* change store enums

* remove all useMobxStore references

* fix more build errors

* dev: reverted workspace invitation

* fix: build errors and warnings

* fix: optimistic update for instant operations (#3221)

* fix: update functions failed case

* fix: typo

* chore: revert back to optimistic update approach for all `update related actions` (#3219)

* fix: merge conflicts resolved

* chore: update memberMap logic in components

* add assignees to kanban groups and properties

* dev: migration fixes

* final bit of optimization on list view

* change all TODOs that are to be done before this release to FIXME

* change base Kanban TODOs that are to be done before this release to FIXME

* dev: add fields and expand for app serializers

* dev: issue detail store

* dev: update issue serializer to return object ids

* fix: Instance key added in settings and converted issues list api to arry instead of dict

* fix: removing segway files

* dev: control expand through query parameters

* revert: github importer

* Revert "dev: segway intgegration (#3132)"

This reverts commit 1cc18a0915.

* dev: remove migrations for segway

* dev: issue structure change and created workspacebasemodel

* dev: issue detail serializer

* fix: changed workspace dict

* dev: updated new issue structure

* chore: build fix

* dev: issue detail store refactor

* dev: created list endpoint for issue-relation

* dev: added issue attachments in issue detail store

* dev: added issue activity computed

* fix: build error

* chore: peek overview modal context added

* chore: build error fix

* dev: added sub_issues in issue details store

* dev: added complete issue serializer for sub issues

* dev: resolved type errors in issue root store

* dev: changed the issue relation structure

* chore: new global dropdowns

* chore: build error fix

* chore: cycle and module selection if disabled

* dev: removed unnecessary code from the workspace root

* chore: build error fix

* chore: issue relation remove endpoint

* fix: build error

* dev: typos and implemented issue relation store

* fix: yarn lock updated

* style: update the UI of all the dropdowns

* fix: state store fixes

* fix: key issue

* fix: state store console logs removed

* refactor: member dropdowns

* fix: moving types to packages

* fix: dropdown arrow positioning

* dev: removed logs

* style: label dropdown

* chore: restrict description notifications

* chore: description changes

* chore: update spreadsheet layout dropdowns

* fix: build errors

* chore: duplicate key change

* fix: ui bugs

* chore: relation activity change

* chore: comment activity changes

* chore: blocking issue removal

* chore: added project_id for relation

* chore: issue relation store and component

* chore: issue redirection issue in the issue realtion in detail page

* chore: created activity changed

* chore: issue links new store implementation on the issue detail

* chore: issue relation deletion acitivity changed

* chore: issue attachments new store implementation on the issue detail

* chore: workspace level issues

* fix: build errors

---------

Co-authored-by: rahulramesha <rahulramesham@gmail.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
Co-authored-by: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com>
Co-authored-by: Nikhil <118773738+pablohashescobar@users.noreply.github.com>
Co-authored-by: Manish Gupta <59428681+mguptahub@users.noreply.github.com>
Co-authored-by: Hoang Luan <luandnh98@gmail.com>
Co-authored-by: luanduongtel4vn <hoangluan@tel4vn.com>
Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>
Co-authored-by: M. Palanikannan <73993394+Palanikannan1437@users.noreply.github.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
2024-01-02 18:12:55 +05:30
Prateek ShouryaandGitHub 1539340113 chore: date and time standardization all across the platform. (#3283)
* chore: date and time standardization all across the platform.

* chore: update `renderFormattedTime` function.
* remove unwanted code.

* fix: build errors

* chore: update `renderFormattedTime` function params.
2024-01-02 14:45:51 +05:30
Anmol Singh BhatiaandGitHub d9ee692ce9 chore: gpt modal refactor (#3276)
* chore: gpt modal refactor

* chore: refactored gpt assistant modal to popover component
2024-01-02 13:07:12 +05:30
sriram veeraghanta 6e702d6cc7 fix: project settings form not loading new data while switching between projects 2023-12-30 21:32:18 +05:30
sriram veeraghanta 447a8bc2f8 Merge branch 'master' of github.com:makeplane/plane into preview 2023-12-30 18:25:53 +05:30
sriram veeraghantaandGitHub 34d872beca Merge pull request #3288 from makeplane/develop
Style: custom theme UI fixes (#3284)
2023-12-30 11:07:04 +05:30
Prateek ShouryaandGitHub 4263e9b507 Style: custom theme UI fixes (#3284)
* style: fix `background-color` inconsistency in issue layout when custom theme is applied.

* fix: theme dropdown overlapping with input color picker icons (z-index issue).
2023-12-30 11:05:15 +05:30
sriram veeraghantaandGitHub 768b4abf22 Merge pull request #3287 from makeplane/preview
promote: preview to master
2023-12-29 22:11:37 +05:30
sriram veeraghantaandGitHub 6f0af4689b Merge pull request #3286 from makeplane/develop
promote: develop to preview
2023-12-29 22:10:24 +05:30
sriram veeraghanta 22bbdd5ab8 fix: add validations 2023-12-29 22:07:52 +05:30
sriram veeraghantaandGitHub 28a9c53202 Merge pull request #3281 from makeplane/preview
release: preview to master
2023-12-29 20:28:18 +05:30
sriram veeraghantaandGitHub 33d88f56da Merge pull request #3280 from makeplane/develop
promote: develop changes to preview
2023-12-29 18:21:26 +05:30
sriram veeraghanta 23001d425f Merge branch 'preview' of github.com:makeplane/plane into develop 2023-12-29 18:16:08 +05:30
sriram veeraghantaandGitHub 5de94c575a fix: workspace invitation status fixes (#3279) 2023-12-29 17:49:39 +05:30
AbId KhAnandGitHub f200acc1e8 fix websocket connection issue in docker makeplane/plane#3195 (#3196)
* fix websocket connection issue in docker makeplane/plane#3195

* fix websocket connection issue for local env with removing from the prod nginx conf template makeplane/plane#319
2023-12-29 16:49:09 +05:30
Prateek ShouryaandGitHub f132fe59ae Chore/user role validation (#3260)
* chore: remove `Save View` option for guest/ viewer from project issues header.

* chore: allow guest/ viewer to clear applied filters in project issues.

* chore: disable `date`, `label` property access for guests/ viewer in all project issue views.

* chore: update `Workspace Issues` -> `All Issues` header.

* chore: refactor apply/ clear filter implementation in All Issues.

* Revert "chore: refactor apply/ clear filter implementation in All Issues."

This reverts commit 024822d54f.

* Revert "chore: allow guest/ viewer to clear applied filters in project issues."

This reverts commit 3dae871d23.

* Revert "chore: update `Workspace Issues` -> `All Issues` header."

This reverts commit 03f90be188.

* chore: remove `cursor-pointer` style from non actionable issue properties.
2023-12-29 16:35:03 +05:30
NikhilandGitHub e584259c04 feat: add external id and external source field to issue, cycle, module, label, comment, state and inbox issues (#3277) 2023-12-29 16:32:12 +05:30
sriram veeraghanta a77ceb636f Merge branches 'develop' and 'develop' of github.com:makeplane/plane into develop 2023-12-29 16:25:54 +05:30
10ab081a0b chore: cycle current status (#3270)
* dev: cycle status

* chore: cycle status logic updated

---------

Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
2023-12-29 15:24:07 +05:30
Anmol Singh BhatiaandGitHub 1d5a3a02c1 fix: issue parent select (#3267) 2023-12-29 15:22:30 +05:30
sriram veeraghantaandGitHub 62b9b259c0 fix: adding issue title to the activity (#3271) 2023-12-28 18:53:06 +05:30
Anmol Singh BhatiaandGitHub 91e84aede1 fix: onboarding redirection loop and bug fixes (#3250)
* chore: try and catch added in handleSignInRedirection

* chore: remove unnecessary hooks

* fix: handleCopyIssueLink url updated

* chore: swap next_url with next_path and validate redirection logic for next_path url
2023-12-28 17:17:04 +05:30
Prateek ShouryaandGitHub 71bf049e89 Style/UI improvements (#3269)
* style: update `Workspace Issues` -> `All Issues` header.

* style: fix issue activity text overflow issue.
2023-12-28 14:30:33 +05:30
sriram veeraghantaandGitHub 685e62a72f fix: replacing onclick redirections with link tag. (#3263)
* fix: adding links to dashboard summary items

* fix: adding links to workspace sidebar dropdown

* fix: adding links to the sidebar
2023-12-28 14:29:20 +05:30
sriram veeraghanta d9bd43f43c fix: adding links to the sidebar 2023-12-28 11:54:16 +05:30
sriram veeraghanta 10bdcc906c fix: adding links to workspace sidebar dropdown 2023-12-27 16:35:43 +05:30
sriram veeraghanta 54964924f0 fix: adding links to dashboard summary items 2023-12-27 16:15:56 +05:30
sriram veeraghanta ff8008cbed Merge branch 'preview' of github.com:makeplane/plane into develop 2023-12-27 15:37:03 +05:30
78428fb564 chore: estimates and issue parent id fixes (#3188)
* chore: validations and bug fixes

* chore: estimate point character limit validation

---------

Co-authored-by: LAKHAN BAHETI <lakhanbaheti9@gmail.com>
2023-12-27 15:35:15 +05:30
Bavisetti NarayanandGitHub b2824366a8 fix: external api serializer (#3259)
* fix: passed workspace id and project id as context

* fix: issue activity
2023-12-27 15:33:31 +05:30
Anmol Singh BhatiaandGitHub 05eb728c40 chore: cycle & module sidebar improvement (#3251)
* chore: no lead option added in module lead select dropdown

* chore: module lead select dropdown code refactor

* chore: cycle sidebar improvement

* chore: module sidebar improvement

* style: cycle and module sidebar improvement

* style: app sidebar improvement
2023-12-27 15:16:13 +05:30
sriram veeraghantaandGitHub ad2471c5a7 Merge pull request #3258 from makeplane/develop
fix: adding access token to build Pr (#3257)
2023-12-26 19:00:10 +05:30
sriram veeraghantaandGitHub 440cfc0f20 fix: adding access token to build Pr (#3257) 2023-12-26 18:59:24 +05:30
sriram veeraghantaandGitHub 9a32d722dc Merge pull request #3256 from makeplane/develop
promote: develop to preview
2023-12-26 18:22:04 +05:30
sriram veeraghantaandGitHub 37fe9a185c fix: adding token to the build pr workflow (#3254)
* fix: adding token to the build pr workflow

* fix: using automatic github token
2023-12-26 18:13:13 +05:30
sriram veeraghanta eabb31a764 fix: upgrading turbo repo changes 2023-12-26 17:04:59 +05:30
sriram veeraghanta 1e27c7936d fix: eslint dependencies fixes 2023-12-26 17:03:43 +05:30
M. PalanikannanandGitHub 0a617eec26 🔧 fix: Pages mutation on component mount (#3230)
* 🔧 chore: Update revalidate option and add useEffect hook in PageDetailsPage component

- Set the `revalidate` option to `false` in the `commonSwrOptions` object.
- Add an `useEffect` hook that calls the `mutatePageDetails` function with specific options.
- Inside the `useEffect` hook, set `revalidate` to `true`, `populateCache` to `true`, and define a rollback function for error handling.
- If an error occurs during the rollback, display an error alert message using the `actionCompleteAlert` function.
- In the `updatePage` function, ensure that `workspaceSlug`, `projectId`, and `pageId` are all defined before proceeding.

* ♻️ refactor: Update code styling for inline code in CoreEditorExtensions

In the `packages/editor/core/src/ui/extensions/index.tsx` file, the code for inline code styling in the CoreEditorExtensions component has been updated. Previously, it was set to `false`, but now it is defined as an object with specific HTML attributes. The code styling includes a rounded-md background, custom primary color, margin, padding, font styling, and disabling spellcheck. This change improves the visual appearance and readability of inline code elements in the editor.
2023-12-26 16:45:00 +05:30
sriram veeraghantaandGitHub e7468292c7 Merge pull request #3175 from makeplane/preview
release: moving changes from preview to master
2023-12-18 19:29:43 +05:30
1021 changed files with 28046 additions and 43186 deletions
@@ -1,6 +1,6 @@
name: Build Pull Request Contents
on:
on:
pull_request:
types: ["opened", "synchronize"]
@@ -14,13 +14,15 @@ jobs:
steps:
- name: Checkout Repository to Actions
uses: actions/checkout@v3.3.0
with:
token: ${{ secrets.ACCESS_TOKEN }}
- name: Setup Node.js 18.x
uses: actions/setup-node@v2
with:
node-version: 18.x
cache: 'yarn'
cache: "yarn"
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v38
@@ -44,5 +46,3 @@ jobs:
run: |
yarn
yarn build --filter=space
+12 -3
View File
@@ -3,7 +3,7 @@ name: Create Sync Action
on:
pull_request:
branches:
- preview
- develop # Change this to preview
types:
- closed
env:
@@ -33,14 +33,23 @@ jobs:
sudo apt update
sudo apt install gh -y
- name: Push Changes to Target Repo
- name: Create Pull Request
env:
GH_TOKEN: ${{ secrets.ACCESS_TOKEN }}
run: |
TARGET_REPO="${{ secrets.SYNC_TARGET_REPO_NAME }}"
TARGET_BRANCH="${{ secrets.SYNC_TARGET_BRANCH_NAME }}"
TARGET_BASE_BRANCH="${{ secrets.SYNC_TARGET_BASE_BRANCH_NAME }}"
SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}"
git checkout $SOURCE_BRANCH
git remote add target-origin "https://$GH_TOKEN@github.com/$TARGET_REPO.git"
git push target-origin $SOURCE_BRANCH:$TARGET_BRANCH
git push target-origin $SOURCE_BRANCH:$TARGET_BRANCH
PR_TITLE=${{secrets.SYNC_PR_TITLE}}
gh pr create \
--base $TARGET_BASE_BRANCH \
--head $TARGET_BRANCH \
--title "$PR_TITLE" \
--repo $TARGET_REPO
-3
View File
@@ -80,6 +80,3 @@ tmp/
## packages
dist
.temp/
# logs
combined.log
+1 -1
View File
@@ -1,3 +1,3 @@
web: gunicorn -w 4 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:$PORT --max-requests 10000 --max-requests-jitter 1000 --access-logfile -
worker: celery -A plane worker -l info -Q internal_tasks,external_tasks
worker: celery -A plane worker -l info
beat: celery -A plane beat -l INFO
+1 -1
View File
@@ -97,7 +97,7 @@ class BaseSerializer(serializers.ModelSerializer):
exp_serializer = expansion[expand](
getattr(instance, expand)
)
response[expand] = exp_serializer.data
response[expand] = exp_serializer.data
else:
# You might need to handle this case differently
response[expand] = getattr(instance, f"{expand}_id", None)
+2 -2
View File
@@ -96,7 +96,7 @@ class IssueSerializer(BaseSerializer):
if (
data.get("state")
and not State.objects.filter(
project_id=self.context.get("project_id"), pk=data.get("state")
project_id=self.context.get("project_id"), pk=data.get("state").id
).exists()
):
raise serializers.ValidationError(
@@ -107,7 +107,7 @@ class IssueSerializer(BaseSerializer):
if (
data.get("parent")
and not Issue.objects.filter(
workspace_id=self.context.get("workspace_id"), pk=data.get("parent")
workspace_id=self.context.get("workspace_id"), pk=data.get("parent").id
).exists()
):
raise serializers.ValidationError(
+7 -7
View File
@@ -65,18 +65,18 @@ class ModuleSerializer(BaseSerializer):
def create(self, validated_data):
members = validated_data.pop("members", None)
project = self.context["project"]
module = Module.objects.create(**validated_data, project=project)
project_id = self.context["project_id"]
workspace_id = self.context["workspace_id"]
module = Module.objects.create(**validated_data, project_id=project_id)
if members is not None:
ModuleMember.objects.bulk_create(
[
ModuleMember(
module=module,
member=member,
project=project,
workspace=project.workspace,
member_id=str(member),
project_id=project_id,
workspace_id=workspace_id,
created_by=module.created_by,
updated_by=module.updated_by,
)
@@ -97,7 +97,7 @@ class ModuleSerializer(BaseSerializer):
[
ModuleMember(
module=instance,
member=member,
member_id=str(member),
project=instance.project,
workspace=instance.project.workspace,
created_by=instance.created_by,
+36 -40
View File
@@ -281,22 +281,20 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
)
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
issue_activity.apply_async(
args=[],
kwargs={
'type': "cycle.activity.deleted",
'requested_data': json.dumps({
issue_activity.delay(
type="cycle.activity.deleted",
requested_data=json.dumps(
{
"cycle_id": str(pk),
"cycle_name": str(cycle.name),
"issues": [str(issue_id) for issue_id in cycle_issues],
}),
'actor_id': str(request.user.id),
'issue_id': None,
'project_id': str(project_id),
'current_instance': None,
'epoch': int(timezone.now().timestamp()),
},
routing_key='external',
}
),
actor_id=str(request.user.id),
issue_id=None,
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
)
# Delete the cycle
cycle.delete()
@@ -456,21 +454,21 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
)
# Capture Issue Activity
issue_activity.apply_async(
args=[],
kwargs={
'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({
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()),
},
routing_key='external',
"created_cycle_issues": serializers.serialize(
"json", record_to_create
),
}
),
epoch=int(timezone.now().timestamp()),
)
# Return all Cycle Issues
@@ -485,21 +483,19 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
)
issue_id = cycle_issue.issue_id
cycle_issue.delete()
issue_activity.apply_async(
args=[],
kwargs={
'type': "cycle.activity.deleted",
'requested_data': json.dumps({
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()),
},
routing_key='external',
}
),
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)
+19 -27
View File
@@ -142,18 +142,14 @@ class InboxIssueAPIEndpoint(BaseAPIView):
)
# Create an Issue Activity
issue_activity.apply_async(
args=[], # If no positional arguments are required
kwargs={
"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()),
},
routing_key="external",
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
@@ -236,21 +232,17 @@ class InboxIssueAPIEndpoint(BaseAPIView):
# Log all the updates
requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder)
if issue is not None:
issue_activity.apply_async(
args=[],
kwargs={
"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()),
},
routing_key="external",
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:
+85 -118
View File
@@ -207,43 +207,44 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
serializer.save()
# Track the issue
issue_activity.apply_async(
args=[], # If no positional arguments are required
kwargs={
'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()),
},
routing_key='external',
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)
def patch(self, request, slug, project_id, pk=None):
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
project = Project.objects.get(pk=project_id)
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)
serializer = IssueSerializer(
issue,
data=request.data,
context={
"project_id": project_id,
"workspace_id": project.workspace_id,
},
partial=True,
)
if serializer.is_valid():
serializer.save()
issue_activity.apply_async(
args=[],
kwargs={
'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()),
},
routing_key='external',
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)
@@ -254,19 +255,14 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
IssueSerializer(issue).data, cls=DjangoJSONEncoder
)
issue.delete()
issue_activity.apply_async(
args=[],
kwargs={
'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()),
},
routing_key='your_routing_key',
queue='your_queue_name'
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)
@@ -322,11 +318,7 @@ class LabelAPIEndpoint(BaseAPIView):
).data,
)
label = self.get_queryset().get(pk=pk)
serializer = LabelSerializer(
label,
fields=self.fields,
expand=self.expand,
)
serializer = LabelSerializer(label, fields=self.fields, expand=self.expand,)
return Response(serializer.data, status=status.HTTP_200_OK)
def patch(self, request, slug, project_id, pk=None):
@@ -336,6 +328,7 @@ class LabelAPIEndpoint(BaseAPIView):
serializer.save()
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):
label = self.get_queryset().get(pk=pk)
@@ -400,18 +393,14 @@ class IssueLinkAPIEndpoint(BaseAPIView):
project_id=project_id,
issue_id=issue_id,
)
issue_activity.apply_async(
args=[], # If no positional arguments are required
kwargs={
'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()),
},
routing_key='external',
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)
@@ -428,18 +417,14 @@ class IssueLinkAPIEndpoint(BaseAPIView):
serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
issue_activity.apply_async(
args=[], # If no positional arguments are required
kwargs={
'type': "link.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()),
},
routing_key='external',
issue_activity.delay(
type="link.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)
@@ -452,18 +437,14 @@ class IssueLinkAPIEndpoint(BaseAPIView):
IssueLinkSerializer(issue_link).data,
cls=DjangoJSONEncoder,
)
issue_activity.apply_async(
args=[], # If no positional arguments are required
kwargs={
'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()),
},
routing_key='external',
issue_activity.delay(
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()),
)
issue_link.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -535,20 +516,14 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
issue_id=issue_id,
actor=request.user,
)
issue_activity.apply_async(
args=[],
kwargs={
"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()),
},
routing_key="external",
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)
@@ -567,18 +542,14 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
)
if serializer.is_valid():
serializer.save()
issue_activity.apply_async(
args=[],
kwargs={
"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()),
},
routing_key="external",
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)
@@ -592,18 +563,14 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
cls=DjangoJSONEncoder,
)
issue_comment.delete()
issue_activity.apply_async(
args=[], # If no positional arguments are required
kwargs={
"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()),
},
routing_key="external",
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)
@@ -624,7 +591,7 @@ class IssueActivityAPIEndpoint(BaseAPIView):
)
.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)
+39 -43
View File
@@ -121,8 +121,8 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
)
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})
project = Project.objects.get(pk=project_id, workspace__slug=slug)
serializer = ModuleSerializer(data=request.data, context={"project_id": project_id, "workspace_id": project.workspace_id})
if serializer.is_valid():
serializer.save()
module = Module.objects.get(pk=serializer.data["id"])
@@ -132,7 +132,7 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
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)
serializer = ModuleSerializer(module, data=request.data, context={"project_id": project_id}, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
@@ -166,22 +166,20 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
module_issues = list(
ModuleIssue.objects.filter(module_id=pk).values_list("issue", flat=True)
)
issue_activity.apply_async(
args=[],
kwargs={
'type': "module.activity.deleted",
'requested_data': json.dumps({
issue_activity.delay(
type="module.activity.deleted",
requested_data=json.dumps(
{
"module_id": str(pk),
"module_name": str(module.name),
"issues": [str(issue_id) for issue_id in module_issues],
}),
'actor_id': str(request.user.id),
'issue_id': None,
'project_id': str(project_id),
'current_instance': None,
'epoch': int(timezone.now().timestamp()),
},
routing_key='external',
}
),
actor_id=str(request.user.id),
issue_id=None,
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
)
module.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -332,21 +330,21 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
)
# Capture Issue Activity
issue_activity.apply_async(
args=[],
kwargs={
'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({
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()),
},
routing_key='external',
"created_module_issues": serializers.serialize(
"json", record_to_create
),
}
),
epoch=int(timezone.now().timestamp()),
)
return Response(
@@ -359,20 +357,18 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
workspace__slug=slug, project_id=project_id, module_id=module_id, issue_id=issue_id
)
module_issue.delete()
issue_activity.apply_async(
args=[], # If no positional arguments are required
kwargs={
'type': "module.activity.deleted",
'requested_data': json.dumps({
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()),
},
routing_key='external',
}
),
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)
@@ -17,6 +17,7 @@ from .workspace import (
WorkspaceThemeSerializer,
WorkspaceMemberAdminSerializer,
WorkspaceMemberMeSerializer,
WorkspaceUserPropertiesSerializer,
)
from .project import (
ProjectSerializer,
@@ -31,6 +32,7 @@ from .project import (
ProjectDeployBoardSerializer,
ProjectMemberAdminSerializer,
ProjectPublicMemberSerializer,
ProjectMemberRoleSerializer,
)
from .state import StateSerializer, StateLiteSerializer
from .view import GlobalViewSerializer, IssueViewSerializer, IssueViewFavoriteSerializer
@@ -39,6 +41,7 @@ from .cycle import (
CycleIssueSerializer,
CycleFavoriteSerializer,
CycleWriteSerializer,
CycleUserPropertiesSerializer,
)
from .asset import FileAssetSerializer
from .issue import (
@@ -61,6 +64,8 @@ from .issue import (
IssueRelationSerializer,
RelatedIssueSerializer,
IssuePublicSerializer,
IssueRelationLiteSerializer,
)
from .module import (
@@ -69,6 +74,7 @@ from .module import (
ModuleIssueSerializer,
ModuleLinkSerializer,
ModuleFavoriteSerializer,
ModuleUserPropertiesSerializer,
)
from .api import APITokenSerializer, APITokenReadSerializer
+88 -8
View File
@@ -9,11 +9,12 @@ 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)
fields = kwargs.pop("fields", [])
self.expand = kwargs.pop("expand", []) or []
fields = self.expand
# 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)
@@ -47,12 +48,91 @@ class DynamicBaseSerializer(BaseSerializer):
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)
for field in allowed:
if field not in self.fields:
from . import (
WorkspaceLiteSerializer,
ProjectLiteSerializer,
UserLiteSerializer,
StateLiteSerializer,
IssueSerializer,
LabelSerializer,
CycleIssueSerializer,
IssueFlatSerializer,
)
# Remove fields from the serializer that aren't in the 'allowed' list.
for field_name in (existing - allowed):
self.fields.pop(field_name)
# 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,
"assignees": UserLiteSerializer,
"labels": LabelSerializer,
"issue_cycle": CycleIssueSerializer,
"parent": IssueFlatSerializer,
}
self.fields[field] = expansion[field](many=True if field in ["members", "assignees", "labels", "issue_cycle"] else False)
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,
LabelSerializer,
CycleIssueSerializer,
)
# 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,
"assignees": UserLiteSerializer,
"labels": LabelSerializer,
"issue_cycle": CycleIssueSerializer,
}
# 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
+14 -1
View File
@@ -7,7 +7,7 @@ from .user import UserLiteSerializer
from .issue import IssueStateSerializer
from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer
from plane.db.models import Cycle, CycleIssue, CycleFavorite
from plane.db.models import Cycle, CycleIssue, CycleFavorite, CycleUserProperties
class CycleWriteSerializer(BaseSerializer):
@@ -40,6 +40,7 @@ class CycleSerializer(BaseSerializer):
started_estimates = serializers.IntegerField(read_only=True)
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
status = serializers.CharField(read_only=True)
def validate(self, data):
if (
@@ -105,3 +106,15 @@ class CycleFavoriteSerializer(BaseSerializer):
"project",
"user",
]
class CycleUserPropertiesSerializer(BaseSerializer):
class Meta:
model = CycleUserProperties
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"cycle"
"user",
]
@@ -4,6 +4,7 @@ from .base import BaseSerializer
from plane.db.models import Estimate, EstimatePoint
from plane.app.serializers import WorkspaceLiteSerializer, ProjectLiteSerializer
from rest_framework import serializers
class EstimateSerializer(BaseSerializer):
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
@@ -19,6 +20,15 @@ class EstimateSerializer(BaseSerializer):
class EstimatePointSerializer(BaseSerializer):
def validate(self, data):
if not data:
raise serializers.ValidationError("Estimate points are required")
value = data.get("value")
if value and len(value) > 20:
raise serializers.ValidationError("Value can't be more than 20 characters")
return data
class Meta:
model = EstimatePoint
fields = "__all__"
-1
View File
@@ -49,7 +49,6 @@ class IssueStateInboxSerializer(BaseSerializer):
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:
+68 -30
View File
@@ -98,6 +98,12 @@ class IssueCreateSerializer(BaseSerializer):
"updated_at",
]
def to_internal_value(self, data):
issue_type = self.context.get("issue_type") if self.context is not None else None
if data and issue_type == "draft-issue" and not data.get('name'):
data['name'] = 'UNTITLED ISSUE'
return super().to_internal_value(data)
def to_representation(self, instance):
data = super().to_representation(instance)
data['assignees'] = [str(assignee.id) for assignee in instance.assignees.all()]
@@ -278,17 +284,28 @@ class IssueLabelSerializer(BaseSerializer):
]
class IssueRelationLiteSerializer(DynamicBaseSerializer):
project_id = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta:
model = Issue
fields = [
"id",
"project_id",
"sequence_id",
]
read_only_fields = [
"workspace",
"project",
]
class IssueRelationSerializer(BaseSerializer):
issue_detail = IssueProjectLiteSerializer(read_only=True, source="related_issue")
issue_detail = IssueRelationLiteSerializer(read_only=True, source="related_issue")
class Meta:
model = IssueRelation
fields = [
"issue_detail",
"relation_type",
"related_issue",
"issue",
"id"
]
read_only_fields = [
"workspace",
@@ -296,16 +313,12 @@ class IssueRelationSerializer(BaseSerializer):
]
class RelatedIssueSerializer(BaseSerializer):
issue_detail = IssueProjectLiteSerializer(read_only=True, source="issue")
issue_detail = IssueRelationLiteSerializer(read_only=True, source="issue")
class Meta:
model = IssueRelation
fields = [
"issue_detail",
"relation_type",
"related_issue",
"issue",
"id"
]
read_only_fields = [
"workspace",
@@ -512,7 +525,6 @@ class IssueStateSerializer(DynamicBaseSerializer):
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)
@@ -521,32 +533,58 @@ class IssueStateSerializer(DynamicBaseSerializer):
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)
class IssueSerializer(DynamicBaseSerializer):
# ids
project_id = serializers.PrimaryKeyRelatedField(read_only=True)
state_id = serializers.PrimaryKeyRelatedField(read_only=True)
parent_id = serializers.PrimaryKeyRelatedField(read_only=True)
cycle_id = serializers.PrimaryKeyRelatedField(read_only=True)
module_id = serializers.PrimaryKeyRelatedField(read_only=True)
# Many to many
label_ids = serializers.PrimaryKeyRelatedField(read_only=True, many=True, source="labels")
assignee_ids = serializers.PrimaryKeyRelatedField(read_only=True, many=True, source="assignees")
# Count items
sub_issues_count = serializers.IntegerField(read_only=True)
issue_reactions = IssueReactionSerializer(read_only=True, many=True)
attachment_count = serializers.IntegerField(read_only=True)
link_count = serializers.IntegerField(read_only=True)
# is
is_subscribed = serializers.BooleanField(read_only=True)
class Meta:
model = Issue
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"created_by",
"updated_by",
fields = [
"id",
"name",
"state_id",
"description_html",
"sort_order",
"completed_at",
"estimate_point",
"priority",
"start_date",
"target_date",
"sequence_id",
"project_id",
"parent_id",
"cycle_id",
"module_id",
"label_ids",
"assignee_ids",
"sub_issues_count",
"created_at",
"updated_at",
"created_by",
"updated_by",
"attachment_count",
"link_count",
"is_subscribed",
"is_draft",
"archived_at",
]
read_only_fields = fields
class IssueLiteSerializer(DynamicBaseSerializer):
+14 -2
View File
@@ -2,7 +2,7 @@
from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .base import BaseSerializer, DynamicBaseSerializer
from .user import UserLiteSerializer
from .project import ProjectLiteSerializer
from .workspace import WorkspaceLiteSerializer
@@ -14,6 +14,7 @@ from plane.db.models import (
ModuleIssue,
ModuleLink,
ModuleFavorite,
ModuleUserProperties,
)
@@ -159,7 +160,7 @@ class ModuleLinkSerializer(BaseSerializer):
return ModuleLink.objects.create(**validated_data)
class ModuleSerializer(BaseSerializer):
class ModuleSerializer(DynamicBaseSerializer):
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")
@@ -196,3 +197,14 @@ class ModuleFavoriteSerializer(BaseSerializer):
"project",
"user",
]
class ModuleUserPropertiesSerializer(BaseSerializer):
class Meta:
model = ModuleUserProperties
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"module",
"user"
]
@@ -159,6 +159,11 @@ class ProjectMemberAdminSerializer(BaseSerializer):
model = ProjectMember
fields = "__all__"
class ProjectMemberRoleSerializer(DynamicBaseSerializer):
class Meta:
model = ProjectMember
fields = ("id", "role", "member", "project")
class ProjectMemberInviteSerializer(BaseSerializer):
project = ProjectLiteSerializer(read_only=True)
+3 -3
View File
@@ -2,7 +2,7 @@
from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .base import BaseSerializer, DynamicBaseSerializer
from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer
from plane.db.models import GlobalView, IssueView, IssueViewFavorite
@@ -38,7 +38,7 @@ class GlobalViewSerializer(BaseSerializer):
return super().update(instance, validated_data)
class IssueViewSerializer(BaseSerializer):
class IssueViewSerializer(DynamicBaseSerializer):
is_favorite = serializers.BooleanField(read_only=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
@@ -80,4 +80,4 @@ class IssueViewFavoriteSerializer(BaseSerializer):
"workspace",
"project",
"user",
]
]
+15 -4
View File
@@ -2,7 +2,7 @@
from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .base import BaseSerializer, DynamicBaseSerializer
from .user import UserLiteSerializer, UserAdminLiteSerializer
from plane.db.models import (
@@ -13,10 +13,11 @@ from plane.db.models import (
TeamMember,
WorkspaceMemberInvite,
WorkspaceTheme,
WorkspaceUserProperties,
)
class WorkSpaceSerializer(BaseSerializer):
class WorkSpaceSerializer(DynamicBaseSerializer):
owner = UserLiteSerializer(read_only=True)
total_members = serializers.IntegerField(read_only=True)
total_issues = serializers.IntegerField(read_only=True)
@@ -62,7 +63,7 @@ class WorkspaceLiteSerializer(BaseSerializer):
class WorkSpaceMemberSerializer(BaseSerializer):
class WorkSpaceMemberSerializer(DynamicBaseSerializer):
member = UserLiteSerializer(read_only=True)
workspace = WorkspaceLiteSerializer(read_only=True)
@@ -78,7 +79,7 @@ class WorkspaceMemberMeSerializer(BaseSerializer):
fields = "__all__"
class WorkspaceMemberAdminSerializer(BaseSerializer):
class WorkspaceMemberAdminSerializer(DynamicBaseSerializer):
member = UserAdminLiteSerializer(read_only=True)
workspace = WorkspaceLiteSerializer(read_only=True)
@@ -161,3 +162,13 @@ class WorkspaceThemeSerializer(BaseSerializer):
"workspace",
"actor",
]
class WorkspaceUserPropertiesSerializer(BaseSerializer):
class Meta:
model = WorkspaceUserProperties
fields = "__all__"
read_only_fields = [
"workspace",
"user",
]
+7 -1
View File
@@ -7,6 +7,7 @@ from plane.app.views import (
CycleDateCheckEndpoint,
CycleFavoriteViewSet,
TransferCycleIssueEndpoint,
CycleUserPropertiesEndpoint,
)
@@ -44,7 +45,7 @@ urlpatterns = [
name="project-issue-cycle",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/<uuid:pk>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/<uuid:issue_id>/",
CycleIssueViewSet.as_view(
{
"get": "retrieve",
@@ -84,4 +85,9 @@ urlpatterns = [
TransferCycleIssueEndpoint.as_view(),
name="transfer-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/user-properties/",
CycleUserPropertiesEndpoint.as_view(),
name="cycle-user-filters",
)
]
+1 -1
View File
@@ -40,7 +40,7 @@ urlpatterns = [
name="inbox-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:pk>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:issue_id>/",
InboxIssueViewSet.as_view(
{
"get": "retrieve",
+4 -3
View File
@@ -235,7 +235,7 @@ urlpatterns = [
## End Comment Reactions
## IssueProperty
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-display-properties/",
"workspaces/<str:slug>/projects/<uuid:project_id>/user-properties/",
IssueUserDisplayPropertyEndpoint.as_view(),
name="project-issue-display-properties",
),
@@ -275,16 +275,17 @@ urlpatterns = [
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/",
IssueRelationViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="issue-relation",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/<uuid:pk>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/remove-relation/",
IssueRelationViewSet.as_view(
{
"delete": "destroy",
"post": "remove_relation",
}
),
name="issue-relation",
+7 -1
View File
@@ -7,6 +7,7 @@ from plane.app.views import (
ModuleLinkViewSet,
ModuleFavoriteViewSet,
BulkImportModulesEndpoint,
ModuleUserPropertiesEndpoint
)
@@ -44,7 +45,7 @@ urlpatterns = [
name="project-module-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/<uuid:pk>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/<uuid:issue_id>/",
ModuleIssueViewSet.as_view(
{
"get": "retrieve",
@@ -101,4 +102,9 @@ urlpatterns = [
BulkImportModulesEndpoint.as_view(),
name="bulk-modules-create",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/user-properties/",
ModuleUserPropertiesEndpoint.as_view(),
name="cycle-user-filters",
)
]
+1 -1
View File
@@ -5,7 +5,7 @@ from plane.app.views import (
IssueViewViewSet,
GlobalViewViewSet,
GlobalViewIssuesViewSet,
IssueViewFavoriteViewSet,
IssueViewFavoriteViewSet,
)
+12
View File
@@ -18,6 +18,8 @@ from plane.app.views import (
WorkspaceUserProfileEndpoint,
WorkspaceUserProfileIssuesEndpoint,
WorkspaceLabelsEndpoint,
WorkspaceProjectMemberEndpoint,
WorkspaceUserPropertiesEndpoint,
)
@@ -92,6 +94,11 @@ urlpatterns = [
WorkSpaceMemberViewSet.as_view({"get": "list"}),
name="workspace-member",
),
path(
"workspaces/<str:slug>/project-members/",
WorkspaceProjectMemberEndpoint.as_view(),
name="workspace-member-roles",
),
path(
"workspaces/<str:slug>/members/<uuid:pk>/",
WorkSpaceMemberViewSet.as_view(
@@ -195,4 +202,9 @@ urlpatterns = [
WorkspaceLabelsEndpoint.as_view(),
name="workspace-labels",
),
path(
"workspaces/<str:slug>/user-properties/",
WorkspaceUserPropertiesEndpoint.as_view(),
name="workspace-user-filters",
)
]
+4
View File
@@ -45,6 +45,8 @@ from .workspace import (
WorkspaceUserProfileEndpoint,
WorkspaceUserProfileIssuesEndpoint,
WorkspaceLabelsEndpoint,
WorkspaceProjectMemberEndpoint,
WorkspaceUserPropertiesEndpoint,
)
from .state import StateViewSet
from .view import (
@@ -59,6 +61,7 @@ from .cycle import (
CycleDateCheckEndpoint,
CycleFavoriteViewSet,
TransferCycleIssueEndpoint,
CycleUserPropertiesEndpoint,
)
from .asset import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet
from .issue import (
@@ -103,6 +106,7 @@ from .module import (
ModuleIssueViewSet,
ModuleLinkViewSet,
ModuleFavoriteViewSet,
ModuleUserPropertiesEndpoint,
)
from .api import ApiTokenEndpoint
+29
View File
@@ -159,6 +159,21 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
if resolve(self.request.path_info).url_name == "project":
return self.kwargs.get("pk", 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
class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
permission_classes = [
@@ -239,3 +254,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
+75 -50
View File
@@ -11,6 +11,10 @@ from django.db.models import (
Count,
Prefetch,
Sum,
Case,
When,
Value,
CharField,
)
from django.core import serializers
from django.utils import timezone
@@ -29,8 +33,9 @@ from plane.app.serializers import (
CycleFavoriteSerializer,
IssueStateSerializer,
CycleWriteSerializer,
CycleUserPropertiesSerializer,
)
from plane.app.permissions import ProjectEntityPermission
from plane.app.permissions import ProjectEntityPermission, ProjectLitePermission
from plane.db.models import (
User,
Cycle,
@@ -40,6 +45,7 @@ from plane.db.models import (
IssueLink,
IssueAttachment,
Label,
CycleUserProperties,
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.grouper import group_results
@@ -157,6 +163,23 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
),
)
)
.annotate(
status=Case(
When(
Q(start_date__lte=timezone.now())
& Q(end_date__gte=timezone.now()),
then=Value("CURRENT"),
),
When(start_date__gt=timezone.now(), then=Value("UPCOMING")),
When(end_date__lt=timezone.now(), then=Value("COMPLETED")),
When(
Q(start_date__isnull=True) & Q(end_date__isnull=True),
then=Value("DRAFT"),
),
default=Value("DRAFT"),
output_field=CharField(),
)
)
.prefetch_related(
Prefetch(
"issue_cycle__issue__assignees",
@@ -176,8 +199,9 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
def list(self, request, slug, project_id):
queryset = self.get_queryset()
cycle_view = request.GET.get("cycle_view", "all")
fields = [field for field in request.GET.get("fields", "").split(",") if field]
queryset = queryset.order_by("-is_favorite","-created_at")
queryset = queryset.order_by("-is_favorite", "-created_at")
# Current Cycle
if cycle_view == "current":
@@ -281,44 +305,8 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
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
)
# If no matching view is found return all cycles
return Response(
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
)
cycles = CycleSerializer(queryset, many=True).data
return Response(cycles, status=status.HTTP_200_OK)
def create(self, request, slug, project_id):
if (
@@ -550,7 +538,6 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
.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")
@@ -574,10 +561,10 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
.values("count")
)
)
issues = IssueStateSerializer(issues, many=True, fields=fields if fields else None).data
issue_dict = {str(issue["id"]): issue for issue in issues}
return Response(issue_dict, status=status.HTTP_200_OK)
serializer = IssueStateSerializer(
issues, many=True, fields=fields if fields else None
)
return Response(serializer.data, status=status.HTTP_200_OK)
def create(self, request, slug, project_id, cycle_id):
issues = request.data.get("issues", [])
@@ -670,11 +657,13 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
status=status.HTTP_200_OK,
)
def destroy(self, request, slug, project_id, cycle_id, pk):
def destroy(self, request, slug, project_id, cycle_id, issue_id):
cycle_issue = CycleIssue.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, cycle_id=cycle_id
issue_id=issue_id,
workspace__slug=slug,
project_id=project_id,
cycle_id=cycle_id,
)
issue_id = cycle_issue.issue_id
issue_activity.delay(
type="cycle.activity.deleted",
requested_data=json.dumps(
@@ -684,7 +673,7 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
}
),
actor_id=str(self.request.user.id),
issue_id=str(cycle_issue.issue_id),
issue_id=str(issue_id),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
epoch=int(timezone.now().timestamp()),
@@ -805,4 +794,40 @@ class TransferCycleIssueEndpoint(BaseAPIView):
updated_cycles, ["cycle_id"], batch_size=100
)
return Response({"message": "Success"}, status=status.HTTP_200_OK)
return Response({"message": "Success"}, status=status.HTTP_200_OK)
class CycleUserPropertiesEndpoint(BaseAPIView):
permission_classes = [
ProjectLitePermission,
]
def patch(self, request, slug, project_id, cycle_id):
cycle_properties = CycleUserProperties.objects.get(
user=request.user,
cycle_id=cycle_id,
project_id=project_id,
workspace__slug=slug,
)
cycle_properties.filters = request.data.get("filters", cycle_properties.filters)
cycle_properties.display_filters = request.data.get(
"display_filters", cycle_properties.display_filters
)
cycle_properties.display_properties = request.data.get(
"display_properties", cycle_properties.display_properties
)
cycle_properties.save()
serializer = CycleUserPropertiesSerializer(cycle_properties)
return Response(serializer.data, status=status.HTTP_201_CREATED)
def get(self, request, slug, project_id, cycle_id):
cycle_properties, _ = CycleUserProperties.objects.get_or_create(
user=request.user,
project_id=project_id,
cycle_id=cycle_id,
workspace__slug=slug,
)
serializer = CycleUserPropertiesSerializer(cycle_properties)
return Response(serializer.data, status=status.HTTP_200_OK)
+4 -4
View File
@@ -53,11 +53,11 @@ class BulkEstimatePointEndpoint(BaseViewSet):
)
estimate_points = request.data.get("estimate_points", [])
if not len(estimate_points) or len(estimate_points) > 8:
serializer = EstimatePointSerializer(data=request.data.get("estimate_points"), many=True)
if not serializer.is_valid():
return Response(
{"error": "Estimate points are required"},
status=status.HTTP_400_BAD_REQUEST,
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
estimate_serializer = EstimateSerializer(data=request.data.get("estimate"))
+112 -147
View File
@@ -1,7 +1,5 @@
# Python imports
import uuid
import json
import requests
# Third party imports
from rest_framework import status
@@ -9,7 +7,7 @@ from rest_framework.response import Response
# Django imports
from django.db.models import Max, Q
from django.conf import settings
# Module imports
from plane.app.views import BaseAPIView
from plane.db.models import (
@@ -36,15 +34,20 @@ from plane.app.serializers import (
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
from plane.app.permissions import WorkSpaceAdminPermission
from plane.bgtasks.importer_task import service_importer
class ServiceIssueImportSummaryEndpoint(BaseAPIView):
def get(self, request, slug, service):
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"},
@@ -55,10 +58,11 @@ class ServiceIssueImportSummaryEndpoint(BaseAPIView):
integration__provider="github", workspace__slug=slug
)
installtion_id = workspace_integration.config.get("installation_id", False)
access_tokens_url = workspace_integration.metadata.get(
"access_tokens_url", False
)
# Check for the installation id
if not installtion_id:
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."
@@ -66,33 +70,18 @@ class ServiceIssueImportSummaryEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST,
)
# Request segway for the required information
if settings.SEGWAY_BASE_URL:
headers = {
"Content-Type": "application/json",
"x-api-key": settings.SEGWAY_KEY,
}
data = {
"owner": owner,
"repo": repo,
"installationId": installtion_id,
}
res = requests.post(
f"{settings.SEGWAY_BASE_URL}/api/github",
data=json.dumps(data),
headers=headers,
)
if "error" in res.json():
return Response(res.json(), status=status.HTTP_400_BAD_REQUEST)
else:
return Response(
res.json(),
status=status.HTTP_200_OK,
)
return Response(
{"error": "Inetgration service is not available please try later"},
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 = {
@@ -113,35 +102,16 @@ class ServiceIssueImportSummaryEndpoint(BaseAPIView):
email = request.GET.get("email", "")
cloud_hostname = request.GET.get("cloud_hostname", "")
if settings.SEGWAY_BASE_URL:
headers = {
"Content-Type": "application/json",
"x-api-key": settings.SEGWAY_KEY,
}
data = {
"project_key": project_key,
"api_token": api_token,
"email": email,
"cloud_hostname": cloud_hostname,
}
res = requests.post(
f"{settings.SEGWAY_BASE_URL}/api/jira",
data=json.dumps(data),
headers=headers,
)
if "error" in res.json():
return Response(res.json(), status=status.HTTP_400_BAD_REQUEST)
else:
return Response(
res.json(),
status=status.HTTP_200_OK,
)
return Response(
{"error": "Inetgration service is not available please try later"},
status=status.HTTP_400_BAD_REQUEST,
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,
@@ -152,21 +122,7 @@ class ImportServiceEndpoint(BaseAPIView):
permission_classes = [
WorkSpaceAdminPermission,
]
def post(self, request, slug, service):
if service not in ["github", "jira"]:
return Response(
{"error": "Servivce not supported yet"},
status=status.HTTP_400_BAD_REQUEST,
)
if service == "github":
workspace_integration = WorkspaceIntegration.objects.get(
integration__provider="github", workspace__slug=slug
)
installation_id = workspace_integration.config.get("installation_id", False)
project_id = request.data.get("project_id", False)
if not project_id:
@@ -174,84 +130,87 @@ class ImportServiceEndpoint(BaseAPIView):
{"error": "Project ID is required"},
status=status.HTTP_400_BAD_REQUEST,
)
workspace = Workspace.objects.get(slug=slug)
# Validate the data
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,
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,
)
# Update config
if config and service == "github":
config.update({"installation_id": installation_id})
service_importer.delay(service, importer.id)
serializer = ImporterSerializer(importer)
return Response(serializer.data, status=status.HTTP_201_CREATED)
# Get the api token -- # derecated
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,
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,
)
# Create an import
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,
)
# Push it to segway
if settings.SEGWAY_BASE_URL:
headers = {
"Content-Type": "application/json",
"x-api-key": settings.SEGWAY_KEY,
}
data = {
"metadata": metadata,
"data": data,
"config": config,
"workspace_id": str(workspace.id),
"project_id": str(project_id),
"created_by": str(request.user.id),
"importer_id": str(importer.id),
}
res = requests.post(
f"{settings.SEGWAY_BASE_URL}/api/github/import",
data=json.dumps(data),
headers=headers,
)
if "error" in res.json():
importer.status = "failed"
importer.reason = str(res.json())
importer.save()
else:
importer.status = "processing"
importer.save(update_fields=["status"])
else:
importer.status = "failed"
importer.reason = "Segway base url is not present"
importer.save(update_fields=["status", "reason"])
# return the response
serializer = ImporterSerializer(importer)
return Response(serializer.data, status=status.HTTP_201_CREATED)
def get(self, request, slug):
imports = (
Importer.objects.filter(workspace__slug=slug)
@@ -262,7 +221,9 @@ class ImportServiceEndpoint(BaseAPIView):
return Response(serializer.data)
def delete(self, request, slug, service, pk):
importer = Importer.objects.get(pk=pk, service=service, workspace__slug=slug)
importer = Importer.objects.get(
pk=pk, service=service, workspace__slug=slug
)
if importer.imported_data is not None:
# Delete all imported Issues
@@ -280,7 +241,9 @@ class ImportServiceEndpoint(BaseAPIView):
return Response(status=status.HTTP_204_NO_CONTENT)
def patch(self, request, slug, service, pk):
importer = Importer.objects.get(pk=pk, service=service, workspace__slug=slug)
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()
@@ -516,7 +479,9 @@ class BulkImportModulesEndpoint(BaseAPIView):
[
ModuleLink(
module=module,
url=module_data.get("link", {}).get("url", "https://plane.so"),
url=module_data.get("link", {}).get(
"url", "https://plane.so"
),
title=module_data.get("link", {}).get(
"title", "Original Issue"
),
+7 -11
View File
@@ -107,7 +107,6 @@ class InboxIssueViewSet(BaseViewSet):
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")
@@ -204,9 +203,9 @@ class InboxIssueViewSet(BaseViewSet):
serializer = IssueStateInboxSerializer(issue)
return Response(serializer.data, status=status.HTTP_200_OK)
def partial_update(self, request, slug, project_id, inbox_id, pk):
def partial_update(self, request, slug, project_id, inbox_id, issue_id):
inbox_issue = InboxIssue.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
issue_id=issue_id, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
)
# Get the project member
project_member = ProjectMember.objects.get(
@@ -316,19 +315,16 @@ class InboxIssueViewSet(BaseViewSet):
InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK
)
def retrieve(self, request, slug, project_id, inbox_id, pk):
inbox_issue = InboxIssue.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
)
def retrieve(self, request, slug, project_id, inbox_id, issue_id):
issue = Issue.objects.get(
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
pk=issue_id, workspace__slug=slug, project_id=project_id
)
serializer = IssueStateInboxSerializer(issue)
return Response(serializer.data, status=status.HTTP_200_OK)
def destroy(self, request, slug, project_id, inbox_id, pk):
def destroy(self, request, slug, project_id, inbox_id, issue_id):
inbox_issue = InboxIssue.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
issue_id=issue_id, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
)
# Get the project member
project_member = ProjectMember.objects.get(
@@ -350,7 +346,7 @@ class InboxIssueViewSet(BaseViewSet):
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
workspace__slug=slug, project_id=project_id, pk=issue_id
).delete()
inbox_issue.delete()
+11 -25
View File
@@ -1,11 +1,8 @@
# Python improts
import uuid
import requests
import json
# Django imports
from django.contrib.auth.hashers import make_password
from django.conf import settings
# Third party imports
from rest_framework.response import Response
@@ -30,7 +27,6 @@ from plane.utils.integrations.github import (
from plane.app.permissions import WorkSpaceAdminPermission
from plane.utils.integrations.slack import slack_oauth
class IntegrationViewSet(BaseViewSet):
serializer_class = IntegrationSerializer
model = Integration
@@ -50,7 +46,9 @@ class IntegrationViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
serializer = IntegrationSerializer(integration, data=request.data, partial=True)
serializer = IntegrationSerializer(
integration, data=request.data, partial=True
)
if serializer.is_valid():
serializer.save()
@@ -96,30 +94,14 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
{"error": "Installation ID is required"},
status=status.HTTP_400_BAD_REQUEST,
)
# Push it to segway
if settings.SEGWAY_BASE_URL:
headers = {
"Content-Type": "application/json",
"x-api-key": settings.SEGWAY_KEY,
}
data = {"installationId": installation_id}
res = requests.post(
f"{settings.SEGWAY_BASE_URL}/api/github/metadata",
data=json.dumps(data),
headers=headers,
)
if "error" in res.json():
return Response(res.json(), status=status.HTTP_400_BAD_REQUEST)
metadata = res.json()
metadata = get_github_metadata(installation_id)
config = {"installation_id": installation_id}
if provider == "slack":
code = request.data.get("code", False)
if not code:
return Response(
{"error": "Code is required"}, status=status.HTTP_400_BAD_REQUEST
)
return Response({"error": "Code is required"}, status=status.HTTP_400_BAD_REQUEST)
slack_response = slack_oauth(code=code)
@@ -141,7 +123,9 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
is_password_autoset=True,
is_bot=True,
first_name=integration.title,
avatar=integration.avatar_url if integration.avatar_url is not None else "",
avatar=integration.avatar_url
if integration.avatar_url is not None
else "",
)
# Create an API Token for the bot user
@@ -177,7 +161,9 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
)
if workspace_integration.integration.provider == "github":
installation_id = workspace_integration.config.get("installation_id", False)
installation_id = workspace_integration.config.get(
"installation_id", False
)
if installation_id:
delete_github_installation(installation_id=installation_id)
@@ -1,15 +1,8 @@
# Python imports
import json
import requests
# Third party imports
from rest_framework import status
from rest_framework.response import Response
from sentry_sdk import capture_exception
# Django imports
from django.conf import settings
# Module imports
from plane.app.views import BaseViewSet, BaseAPIView
from plane.db.models import (
@@ -42,32 +35,19 @@ class GithubRepositoriesEndpoint(BaseAPIView):
workspace__slug=slug, pk=workspace_integration_id
)
installation_id = workspace_integration.config.get("installation_id")
if not installation_id:
if workspace_integration.integration.provider != "github":
return Response(
{"error": "Not a github integration"},
status=status.HTTP_400_BAD_REQUEST,
)
# Push it to segway
if settings.SEGWAY_BASE_URL:
headers = {
"Content-Type": "application/json",
"x-api-key": settings.SEGWAY_KEY,
}
data = {
"installationId": installation_id,
"page": page,
}
res = requests.post(
f"{settings.SEGWAY_BASE_URL}/api/github/repos",
data=json.dumps(data),
headers=headers,
)
if "error" in res.json():
return Response(res.json(), status=status.HTTP_400_BAD_REQUEST)
else:
return Response(res.json(), status=status.HTTP_200_OK)
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)
class GithubRepositorySyncViewSet(BaseViewSet):
+122 -48
View File
@@ -52,6 +52,7 @@ from plane.app.serializers import (
IssueRelationSerializer,
RelatedIssueSerializer,
IssuePublicSerializer,
IssueRelationLiteSerializer,
)
from plane.app.permissions import (
ProjectEntityPermission,
@@ -129,22 +130,6 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
queryset=IssueReaction.objects.select_related("actor"),
)
)
).distinct()
@method_decorator(gzip_page)
def list(self, request, slug, project_id):
fields = [field for field in request.GET.get("fields", "").split(",") if field]
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(
@@ -159,7 +144,26 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
.annotate(
is_subscribed=Exists(
IssueSubscriber.objects.filter(
subscriber=self.request.user, issue_id=OuterRef("id")
)
)
)
).distinct()
@method_decorator(gzip_page)
def list(self, request, slug, project_id):
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)
# Priority Ordering
if order_by_param == "priority" or order_by_param == "-priority":
@@ -217,9 +221,10 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
else:
issue_queryset = issue_queryset.order_by(order_by_param)
issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data
issue_dict = {str(issue["id"]): issue for issue in issues}
return Response(issue_dict, status=status.HTTP_200_OK)
issues = IssueSerializer(
issue_queryset, many=True, fields=self.fields, expand=self.expand
).data
return Response(issues, status=status.HTTP_200_OK)
def create(self, request, slug, project_id):
project = Project.objects.get(pk=project_id)
@@ -256,7 +261,10 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
.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)
return Response(
IssueSerializer(issue, fields=self.fields, expand=self.expand).data,
status=status.HTTP_200_OK,
)
def partial_update(self, request, slug, project_id, pk=None):
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
@@ -590,16 +598,19 @@ class IssueUserDisplayPropertyEndpoint(BaseAPIView):
ProjectLitePermission,
]
def post(self, request, slug, project_id):
issue_property, created = IssueProperty.objects.get_or_create(
def patch(self, request, slug, project_id):
issue_property = IssueProperty.objects.get(
user=request.user,
project_id=project_id,
)
if not created:
issue_property.properties = request.data.get("properties", {})
issue_property.save()
issue_property.properties = request.data.get("properties", {})
issue_property.filters = request.data.get("filters", issue_property.filters)
issue_property.display_filters = request.data.get(
"display_filters", issue_property.display_filters
)
issue_property.display_properties = request.data.get(
"display_properties", issue_property.display_properties
)
issue_property.save()
serializer = IssuePropertySerializer(issue_property)
return Response(serializer.data, status=status.HTTP_201_CREATED)
@@ -708,6 +719,13 @@ class SubIssuesEndpoint(BaseAPIView):
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
is_subscribed=Exists(
IssueSubscriber.objects.filter(
subscriber=self.request.user, issue_id=OuterRef("id")
)
)
)
.prefetch_related(
Prefetch(
"issue_reactions",
@@ -728,7 +746,7 @@ class SubIssuesEndpoint(BaseAPIView):
item["state_group"]: item["state_count"] for item in state_distribution
}
serializer = IssueLiteSerializer(
serializer = IssueSerializer(
sub_issues,
many=True,
)
@@ -775,7 +793,7 @@ class SubIssuesEndpoint(BaseAPIView):
]
return Response(
IssueFlatSerializer(updated_sub_issues, many=True).data,
IssueSerializer(updated_sub_issues, many=True).data,
status=status.HTTP_200_OK,
)
@@ -1062,9 +1080,10 @@ class IssueArchiveViewSet(BaseViewSet):
else issue_queryset.filter(parent__isnull=True)
)
issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data
issue_dict = {str(issue["id"]): issue for issue in issues}
return Response(issue_dict, status=status.HTTP_200_OK)
issues = IssueLiteSerializer(
issue_queryset, many=True, fields=fields if fields else None
).data
return Response(issues, status=status.HTTP_200_OK)
def retrieve(self, request, slug, project_id, pk=None):
issue = Issue.objects.get(
@@ -1365,23 +1384,68 @@ class IssueRelationViewSet(BaseViewSet):
.distinct()
)
def list(self, request, slug, project_id, issue_id):
issue_relations = (
IssueRelation.objects.filter(Q(issue_id=issue_id) | Q(related_issue=issue_id))
.filter(workspace__slug=self.kwargs.get("slug"))
.select_related("project")
.select_related("workspace")
.select_related("issue")
.order_by("-created_at")
.distinct()
)
blocking_issues = issue_relations.filter(relation_type="blocked_by", related_issue_id=issue_id)
blocked_by_issues = issue_relations.filter(relation_type="blocked_by", issue_id=issue_id)
duplicate_issues = issue_relations.filter(issue_id=issue_id, relation_type="duplicate")
duplicate_issues_related = issue_relations.filter(related_issue_id=issue_id, relation_type="duplicate")
relates_to_issues = issue_relations.filter(issue_id=issue_id, relation_type="relates_to")
relates_to_issues_related = issue_relations.filter(related_issue_id=issue_id, relation_type="relates_to")
blocked_by_issues_serialized = IssueRelationSerializer(blocked_by_issues, many=True).data
duplicate_issues_serialized = IssueRelationSerializer(duplicate_issues, many=True).data
relates_to_issues_serialized = IssueRelationSerializer(relates_to_issues, many=True).data
# revere relation for blocked by issues
blocking_issues_serialized = RelatedIssueSerializer(blocking_issues, many=True).data
# reverse relation for duplicate issues
duplicate_issues_related_serialized = RelatedIssueSerializer(duplicate_issues_related, many=True).data
# reverse relation for related issues
relates_to_issues_related_serialized = RelatedIssueSerializer(relates_to_issues_related, many=True).data
response_data = {
'blocking': blocking_issues_serialized,
'blocked_by': blocked_by_issues_serialized,
'duplicate': duplicate_issues_serialized + duplicate_issues_related_serialized,
'relates_to': relates_to_issues_serialized + relates_to_issues_related_serialized,
}
return Response(response_data, status=status.HTTP_200_OK)
def create(self, request, slug, project_id, issue_id):
related_list = request.data.get("related_list", [])
relation = request.data.get("relation", None)
relation_type = request.data.get("relation_type", None)
issues = request.data.get("issues", [])
project = Project.objects.get(pk=project_id)
# If the current issue is duplicate of another issue then the current issue's state will be marked as duplicate
if relation_type == "duplicate":
issue = Issue.objects.get(pk=issue_id)
issue.state = State.objects.get(group="cancelled", workspace__slug=slug, project_id=project_id)
issue.save(update_fields=["state"])
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"],
issue_id=issue if relation_type == "blocking" else issue_id,
related_issue_id=issue_id if relation_type == "blocking" else issue,
relation_type="blocked_by" if relation_type == "blocking" else 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
for issue in issues
],
batch_size=10,
ignore_conflicts=True,
@@ -1397,7 +1461,7 @@ class IssueRelationViewSet(BaseViewSet):
epoch=int(timezone.now().timestamp()),
)
if relation == "blocking":
if relation_type == "blocking":
return Response(
RelatedIssueSerializer(issue_relation, many=True).data,
status=status.HTTP_201_CREATED,
@@ -1408,10 +1472,18 @@ class IssueRelationViewSet(BaseViewSet):
status=status.HTTP_201_CREATED,
)
def destroy(self, request, slug, project_id, issue_id, pk):
issue_relation = IssueRelation.objects.get(
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
)
def remove_relation(self, request, slug, project_id, issue_id):
relation_type = request.data.get("relation_type", None)
related_issue = request.data.get("related_issue", None)
if relation_type == "blocking":
issue_relation = IssueRelation.objects.get(
workspace__slug=slug, project_id=project_id, issue_id=related_issue, related_issue_id=issue_id
)
else:
issue_relation = IssueRelation.objects.get(
workspace__slug=slug, project_id=project_id, issue_id=issue_id, related_issue_id=related_issue
)
current_instance = json.dumps(
IssueRelationSerializer(issue_relation).data,
cls=DjangoJSONEncoder,
@@ -1419,7 +1491,7 @@ class IssueRelationViewSet(BaseViewSet):
issue_relation.delete()
issue_activity.delay(
type="issue_relation.activity.deleted",
requested_data=json.dumps({"related_list": None}),
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
actor_id=str(request.user.id),
issue_id=str(issue_id),
project_id=str(project_id),
@@ -1547,9 +1619,10 @@ class IssueDraftViewSet(BaseViewSet):
else:
issue_queryset = issue_queryset.order_by(order_by_param)
issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data
issue_dict = {str(issue["id"]): issue for issue in issues}
return Response(issue_dict, status=status.HTTP_200_OK)
issues = IssueLiteSerializer(
issue_queryset, many=True, fields=fields if fields else None
).data
return Response(issues, status=status.HTTP_200_OK)
def create(self, request, slug, project_id):
project = Project.objects.get(pk=project_id)
@@ -1557,6 +1630,7 @@ class IssueDraftViewSet(BaseViewSet):
serializer = IssueCreateSerializer(
data=request.data,
context={
"issue_type": "draft-issue",
"project_id": project_id,
"workspace_id": project.workspace_id,
"default_assignee_id": project.default_assignee_id,
@@ -1626,4 +1700,4 @@ class IssueDraftViewSet(BaseViewSet):
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
)
return Response(status=status.HTTP_204_NO_CONTENT)
return Response(status=status.HTTP_204_NO_CONTENT)
+62 -13
View File
@@ -21,8 +21,9 @@ from plane.app.serializers import (
ModuleLinkSerializer,
ModuleFavoriteSerializer,
IssueStateSerializer,
ModuleUserPropertiesSerializer,
)
from plane.app.permissions import ProjectEntityPermission
from plane.app.permissions import ProjectEntityPermission, ProjectLitePermission
from plane.db.models import (
Module,
ModuleIssue,
@@ -32,6 +33,7 @@ from plane.db.models import (
ModuleFavorite,
IssueLink,
IssueAttachment,
ModuleUserProperties,
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.grouper import group_results
@@ -54,7 +56,6 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
)
def get_queryset(self):
subquery = ModuleFavorite.objects.filter(
user=self.request.user,
module_id=OuterRef("pk"),
@@ -136,7 +137,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
),
)
)
.order_by("-is_favorite","-created_at")
.order_by("-is_favorite", "-created_at")
)
def create(self, request, slug, project_id):
@@ -153,6 +154,14 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def list(self, request, slug, project_id):
queryset = self.get_queryset()
fields = [field for field in request.GET.get("fields", "").split(",") if field]
modules = ModuleSerializer(
queryset, many=True, fields=fields if fields else None
).data
return Response(modules, status=status.HTTP_200_OK)
def retrieve(self, request, slug, project_id, pk):
queryset = self.get_queryset().get(pk=pk)
@@ -289,7 +298,6 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
webhook_event = "module_issue"
bulk = True
filterset_fields = [
"issue__labels__id",
"issue__assignees__id",
@@ -335,7 +343,6 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
.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")
@@ -359,9 +366,10 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
.values("count")
)
)
issues = IssueStateSerializer(issues, many=True, fields=fields if fields else None).data
issue_dict = {str(issue["id"]): issue for issue in issues}
return Response(issue_dict, status=status.HTTP_200_OK)
serializer = IssueStateSerializer(
issues, many=True, fields=fields if fields else None
)
return Response(serializer.data, status=status.HTTP_200_OK)
def create(self, request, slug, project_id, module_id):
issues = request.data.get("issues", [])
@@ -444,20 +452,23 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
status=status.HTTP_200_OK,
)
def destroy(self, request, slug, project_id, module_id, pk):
def destroy(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, pk=pk
workspace__slug=slug,
project_id=project_id,
module_id=module_id,
issue_id=issue_id,
)
issue_activity.delay(
type="module.activity.deleted",
requested_data=json.dumps(
{
"module_id": str(module_id),
"issues": [str(module_issue.issue_id)],
"issues": [str(issue_id)],
}
),
actor_id=str(request.user.id),
issue_id=str(module_issue.issue_id),
issue_id=str(issue_id),
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
@@ -521,4 +532,42 @@ class ModuleFavoriteViewSet(BaseViewSet):
module_id=module_id,
)
module_favorite.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
return Response(status=status.HTTP_204_NO_CONTENT)
class ModuleUserPropertiesEndpoint(BaseAPIView):
permission_classes = [
ProjectLitePermission,
]
def patch(self, request, slug, project_id, module_id):
module_properties = ModuleUserProperties.objects.get(
user=request.user,
module_id=module_id,
project_id=project_id,
workspace__slug=slug,
)
module_properties.filters = request.data.get(
"filters", module_properties.filters
)
module_properties.display_filters = request.data.get(
"display_filters", module_properties.display_filters
)
module_properties.display_properties = request.data.get(
"display_properties", module_properties.display_properties
)
module_properties.save()
serializer = ModuleUserPropertiesSerializer(module_properties)
return Response(serializer.data, status=status.HTTP_201_CREATED)
def get(self, request, slug, project_id, module_id):
module_properties, _ = ModuleUserProperties.objects.get_or_create(
user=request.user,
project_id=project_id,
module_id=module_id,
workspace__slug=slug,
)
serializer = ModuleUserPropertiesSerializer(module_properties)
return Response(serializer.data, status=status.HTTP_200_OK)
+5 -6
View File
@@ -157,9 +157,8 @@ class PageViewSet(BaseViewSet):
def list(self, request, slug, project_id):
queryset = self.get_queryset().filter(archived_at__isnull=True)
return Response(
PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK
)
pages = PageSerializer(queryset, many=True).data
return Response(pages, status=status.HTTP_200_OK)
def archive(self, request, slug, project_id, page_id):
page = Page.objects.get(pk=page_id, workspace__slug=slug, project_id=project_id)
@@ -210,9 +209,9 @@ class PageViewSet(BaseViewSet):
workspace__slug=slug,
).filter(archived_at__isnull=False)
return Response(
PageSerializer(pages, many=True).data, status=status.HTTP_200_OK
)
pages = PageSerializer(pages, many=True).data
return Response(pages, status=status.HTTP_200_OK)
def destroy(self, request, slug, project_id, pk):
page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
+13 -41
View File
@@ -36,6 +36,7 @@ from plane.app.serializers import (
ProjectFavoriteSerializer,
ProjectDeployBoardSerializer,
ProjectMemberAdminSerializer,
ProjectMemberRoleSerializer,
)
from plane.app.permissions import (
@@ -180,12 +181,9 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
projects, many=True
).data,
)
projects = ProjectListSerializer(projects, many=True, fields=fields if fields else None).data
return Response(projects, status=status.HTTP_200_OK)
return Response(
ProjectListSerializer(
projects, many=True, fields=fields if fields else None
).data
)
def create(self, request, slug):
try:
@@ -713,13 +711,7 @@ class ProjectMemberViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_201_CREATED)
def list(self, request, slug, project_id):
project_member = ProjectMember.objects.get(
member=request.user,
workspace__slug=slug,
project_id=project_id,
is_active=True,
)
# Get the list of project members for the project
project_members = ProjectMember.objects.filter(
project_id=project_id,
workspace__slug=slug,
@@ -727,10 +719,7 @@ class ProjectMemberViewSet(BaseViewSet):
is_active=True,
).select_related("project", "member", "workspace")
if project_member.role > 10:
serializer = ProjectMemberAdminSerializer(project_members, many=True)
else:
serializer = ProjectMemberSerializer(project_members, many=True)
serializer = ProjectMemberRoleSerializer(project_members, fields=("id", "member", "role"), many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
def partial_update(self, request, slug, project_id, pk):
@@ -1010,18 +999,11 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
def get(self, request):
files = []
s3_client_params = {
"service_name": "s3",
"aws_access_key_id": settings.AWS_ACCESS_KEY_ID,
"aws_secret_access_key": settings.AWS_SECRET_ACCESS_KEY,
}
# Use AWS_S3_ENDPOINT_URL if it is present in the settings
if hasattr(settings, "AWS_S3_ENDPOINT_URL") and settings.AWS_S3_ENDPOINT_URL:
s3_client_params["endpoint_url"] = settings.AWS_S3_ENDPOINT_URL
s3 = boto3.client(**s3_client_params)
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_STORAGE_BUCKET_NAME,
"Prefix": "static/project-cover/",
@@ -1034,19 +1016,9 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
if not content["Key"].endswith(
"/"
): # This line ensures we're only getting files, not "sub-folders"
if (
hasattr(settings, "AWS_S3_CUSTOM_DOMAIN")
and settings.AWS_S3_CUSTOM_DOMAIN
and hasattr(settings, "AWS_S3_URL_PROTOCOL")
and settings.AWS_S3_URL_PROTOCOL
):
files.append(
f"{settings.AWS_S3_URL_PROTOCOL}//{settings.AWS_S3_CUSTOM_DOMAIN}/{content['Key']}"
)
else:
files.append(
f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}"
)
files.append(
f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}"
)
return Response(files, status=status.HTTP_200_OK)
+2 -1
View File
@@ -50,7 +50,8 @@ class GlobalSearchEndpoint(BaseAPIView):
q = Q()
for field in fields:
if field == "sequence_id":
sequences = re.findall(r"\d+\.\d+|\d+", query)
# Match whole integers only (exclude decimal numbers)
sequences = re.findall(r"\b\d+\b", query)
for sequence_id in sequences:
q |= Q(**{"sequence_id": sequence_id})
else:
+21 -9
View File
@@ -27,7 +27,12 @@ from plane.app.serializers import (
IssueLiteSerializer,
IssueViewFavoriteSerializer,
)
from plane.app.permissions import WorkspaceEntityPermission, ProjectEntityPermission
from plane.app.permissions import (
WorkspaceEntityPermission,
ProjectEntityPermission,
WorkspaceViewerPermission,
ProjectLitePermission,
)
from plane.db.models import (
Workspace,
GlobalView,
@@ -43,8 +48,8 @@ from plane.utils.grouper import group_results
class GlobalViewViewSet(BaseViewSet):
serializer_class = GlobalViewSerializer
model = GlobalView
serializer_class = IssueViewSerializer
model = IssueView
permission_classes = [
WorkspaceEntityPermission,
]
@@ -58,6 +63,7 @@ class GlobalViewViewSet(BaseViewSet):
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project__isnull=True)
.select_related("workspace")
.order_by(self.request.GET.get("order_by", "-created_at"))
.distinct()
@@ -179,12 +185,10 @@ class GlobalViewIssuesViewSet(BaseViewSet):
else:
issue_queryset = issue_queryset.order_by(order_by_param)
issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data
issue_dict = {str(issue["id"]): issue for issue in issues}
return Response(
issue_dict,
status=status.HTTP_200_OK,
serializer = IssueLiteSerializer(
issue_queryset, many=True, fields=fields if fields else None
)
return Response(serializer.data, status=status.HTTP_200_OK)
class IssueViewViewSet(BaseViewSet):
@@ -217,6 +221,14 @@ class IssueViewViewSet(BaseViewSet):
.distinct()
)
def list(self, request, slug, project_id):
queryset = self.get_queryset()
fields = [field for field in request.GET.get("fields", "").split(",") if field]
views = IssueViewSerializer(
queryset, many=True, fields=fields if fields else None
).data
return Response(views, status=status.HTTP_200_OK)
class IssueViewFavoriteViewSet(BaseViewSet):
serializer_class = IssueViewFavoriteSerializer
@@ -246,4 +258,4 @@ class IssueViewFavoriteViewSet(BaseViewSet):
view_id=view_id,
)
view_favourite.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
return Response(status=status.HTTP_204_NO_CONTENT)
+84 -7
View File
@@ -44,6 +44,8 @@ from plane.app.serializers import (
IssueLiteSerializer,
WorkspaceMemberAdminSerializer,
WorkspaceMemberMeSerializer,
ProjectMemberRoleSerializer,
WorkspaceUserPropertiesSerializer,
)
from plane.app.views.base import BaseAPIView
from . import BaseViewSet
@@ -64,6 +66,7 @@ from plane.db.models import (
WorkspaceMember,
CycleIssue,
IssueReaction,
WorkspaceUserProperties
)
from plane.app.permissions import (
WorkSpaceBasePermission,
@@ -71,11 +74,13 @@ from plane.app.permissions import (
WorkspaceEntityPermission,
WorkspaceViewerPermission,
WorkspaceUserPermission,
ProjectLitePermission,
)
from plane.bgtasks.workspace_invitation_task import workspace_invitation
from plane.utils.issue_filters import issue_filters
from plane.bgtasks.event_tracking_task import workspace_invite_event
class WorkSpaceViewSet(BaseViewSet):
model = Workspace
serializer_class = WorkSpaceSerializer
@@ -173,6 +178,7 @@ class UserWorkSpacesEndpoint(BaseAPIView):
]
def get(self, request):
fields = [field for field in request.GET.get("fields", "").split(",") if field]
member_count = (
WorkspaceMember.objects.filter(
workspace=OuterRef("id"),
@@ -208,9 +214,12 @@ class UserWorkSpacesEndpoint(BaseAPIView):
)
.distinct()
)
serializer = WorkSpaceSerializer(self.filter_queryset(workspace), many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
workspaces = WorkSpaceSerializer(
self.filter_queryset(workspace),
fields=fields if fields else None,
many=True,
).data
return Response(workspaces, status=status.HTTP_200_OK)
class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView):
@@ -407,7 +416,7 @@ class WorkspaceJoinEndpoint(BaseAPIView):
# Delete the invitation
workspace_invite.delete()
# Send event
workspace_invite_event.delay(
user=user.id if user is not None else None,
@@ -537,10 +546,15 @@ class WorkSpaceMemberViewSet(BaseViewSet):
workspace_members = self.get_queryset()
if workspace_member.role > 10:
serializer = WorkspaceMemberAdminSerializer(workspace_members, many=True)
serializer = WorkspaceMemberAdminSerializer(
workspace_members,
fields=("id", "member", "role"),
many=True,
)
else:
serializer = WorkSpaceMemberSerializer(
workspace_members,
fields=("id", "member", "role"),
many=True,
)
return Response(serializer.data, status=status.HTTP_200_OK)
@@ -705,6 +719,43 @@ class WorkSpaceMemberViewSet(BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT)
class WorkspaceProjectMemberEndpoint(BaseAPIView):
serializer_class = ProjectMemberRoleSerializer
model = ProjectMember
permission_classes = [
WorkspaceEntityPermission,
]
def get(self, request, slug):
# Fetch all project IDs where the user is involved
project_ids = ProjectMember.objects.filter(
member=request.user,
member__is_bot=False,
is_active=True,
).values_list('project_id', flat=True).distinct()
# Get all the project members in which the user is involved
project_members = ProjectMember.objects.filter(
workspace__slug=slug,
member__is_bot=False,
project_id__in=project_ids,
is_active=True,
).select_related("project", "member", "workspace")
project_members = ProjectMemberRoleSerializer(project_members, many=True).data
project_members_dict = dict()
# Construct a dictionary with project_id as key and project_members as value
for project_member in project_members:
project_id = project_member.pop("project")
if str(project_id) not in project_members_dict:
project_members_dict[str(project_id)] = []
project_members_dict[str(project_id)].append(project_member)
return Response(project_members_dict, status=status.HTTP_200_OK)
class TeamMemberViewSet(BaseViewSet):
serializer_class = TeamSerializer
model = Team
@@ -1334,8 +1385,7 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
issues = IssueLiteSerializer(
issue_queryset, many=True, fields=fields if fields else None
).data
issue_dict = {str(issue["id"]): issue for issue in issues}
return Response(issue_dict, status=status.HTTP_200_OK)
return Response(issues, status=status.HTTP_200_OK)
class WorkspaceLabelsEndpoint(BaseAPIView):
@@ -1349,3 +1399,30 @@ class WorkspaceLabelsEndpoint(BaseAPIView):
project__project_projectmember__member=request.user,
).values("parent", "name", "color", "id", "project_id", "workspace__slug")
return Response(labels, status=status.HTTP_200_OK)
class WorkspaceUserPropertiesEndpoint(BaseAPIView):
permission_classes = [
WorkspaceViewerPermission,
]
def patch(self, request, slug):
workspace_properties = WorkspaceUserProperties.objects.get(
user=request.user,
workspace__slug=slug,
)
workspace_properties.filters = request.data.get("filters", workspace_properties.filters)
workspace_properties.display_filters = request.data.get("display_filters", workspace_properties.display_filters)
workspace_properties.display_properties = request.data.get("display_properties", workspace_properties.display_properties)
workspace_properties.save()
serializer = WorkspaceUserPropertiesSerializer(workspace_properties)
return Response(serializer.data, status=status.HTTP_201_CREATED)
def get(self, request, slug):
workspace_properties, _ = WorkspaceUserProperties.objects.get_or_create(
user=request.user, workspace__slug=slug
)
serializer = WorkspaceUserPropertiesSerializer(workspace_properties)
return Response(serializer.data, status=status.HTTP_200_OK)
-1
View File
@@ -1 +0,0 @@
from .issue_sync_task import issue_sync
@@ -373,7 +373,7 @@ def generate_non_segmented_rows(
return [tuple(row_zero)] + rows
@shared_task(queue='internal_tasks')
@shared_task
def analytic_export_task(email, data, slug):
try:
filters = issue_filters(data, "POST")
@@ -29,7 +29,7 @@ def posthogConfiguration():
return None, None
@shared_task(queue='internal_tasks')
@shared_task
def auth_events(user, email, user_agent, ip, event_name, medium, first_time):
try:
POSTHOG_API_KEY, POSTHOG_HOST = posthogConfiguration()
@@ -54,7 +54,7 @@ def auth_events(user, email, user_agent, ip, event_name, medium, first_time):
capture_exception(e)
@shared_task(queue='internal_tasks')
@shared_task
def workspace_invite_event(user, email, user_agent, ip, event_name, accepted_from):
try:
POSTHOG_API_KEY, POSTHOG_HOST = posthogConfiguration()
+1 -1
View File
@@ -259,7 +259,7 @@ def generate_xlsx(header, project_id, issues, files):
files.append((f"{project_id}.xlsx", xlsx_file))
@shared_task(queue='internal_tasks')
@shared_task
def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, slug):
try:
exporter_instance = ExporterHistory.objects.get(token=token_id)
@@ -15,7 +15,7 @@ from botocore.client import Config
from plane.db.models import ExporterHistory
@shared_task(queue='internal_tasks')
@shared_task
def delete_old_s3_link():
# Get a list of keys and IDs to process
expired_exporter_history = ExporterHistory.objects.filter(
+1 -1
View File
@@ -12,7 +12,7 @@ from celery import shared_task
from plane.db.models import FileAsset
@shared_task(queue='internal_tasks')
@shared_task
def delete_file_asset():
# file assets to delete
@@ -17,7 +17,7 @@ from sentry_sdk import capture_exception
from plane.license.utils.instance_value import get_email_configuration
@shared_task(queue='internal_tasks')
@shared_task
def forgot_password(first_name, email, uidb64, token, current_site):
try:
relative_link = (
+159 -426
View File
@@ -1,467 +1,200 @@
# Python imports
import json
import requests
import uuid
# Django imports
from django.db.models import Q, Max
from django.conf import settings
from django.core.serializers.json import DjangoJSONEncoder
from django.contrib.auth.hashers import make_password
# Third Party imports
from celery import shared_task
from celery.exceptions import MaxRetriesExceededError
from sentry_sdk import capture_exception
# Module imports
from plane.app.serializers import ImporterSerializer
from plane.db.models import (
Importer,
WorkspaceMember,
GithubRepositorySync,
GithubRepository,
ProjectMember,
WorkspaceIntegration,
Label,
User,
IssueProperty,
IssueAssignee,
IssueLabel,
IssueSequence,
IssueActivity,
IssueComment,
IssueLink,
ModuleIssue,
State,
Module,
Issue,
Cycle,
)
from plane.bgtasks.user_welcome_task import send_welcome_slack
@shared_task(queue="internal_tasks")
@shared_task
def service_importer(service, importer_id):
pass
## Utility functions
def get_label_id(name, data):
try:
existing_label = (
Label.objects.filter(
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
name__iexact=name,
)
.values("id")
.first()
)
return existing_label
except Label.DoesNotExist:
return None
importer = Importer.objects.get(pk=importer_id)
importer.status = "processing"
importer.save()
users = importer.data.get("users", [])
def get_state_id(name, data):
try:
existing_state = (
State.objects.filter(
name__iexact=name,
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
)
.values("id")
.first()
)
return existing_state
except State.DoesNotExist:
return None
def get_user_id(name):
try:
existing_user = User.objects.filter(email=name).values("id").first()
return existing_user
except User.DoesNotExist:
return None
def update_imported_items(importer_id, entity, entity_id):
importer = Importer.objects.get(pk=importer_id)
if importer.imported_data:
importer.imported_data.setdefault(str(entity), []).append(str(entity_id))
else:
importer.imported_data = {
str(entity): [str(entity_id)]
}
importer.save()
## Sync functions
def members_sync(data):
try:
user = User.objects.get(email=data.get("email"))
_ = WorkspaceMember.objects.get_or_create(
member_id=user.id, workspace_id=data.get("workspace_id")
)
_ = ProjectMember.objects.get_or_create(
member_id=user.id,
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
)
_ = IssueProperty.objects.get_or_create(
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
user_id=user.id,
created_by_id=data.get("created_by"),
)
except User.DoesNotExist:
# For all invited users create the users
new_user = User.objects.create(
email=data.get("email").strip().lower(),
username=uuid.uuid4().hex,
password=make_password(uuid.uuid4().hex),
is_password_autoset=True,
)
service = data.get("external_source")
WorkspaceMember.objects.create(
member_id=new_user.id,
workspace_id=data.get("workspace_id"),
created_by_id=data.get("created_by"),
)
ProjectMember.objects.create(
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
member_id=new_user.id,
created_by_id=data.get("created_by"),
)
IssueProperty.objects.create(
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
user_id=new_user.id,
created_by_id=data.get("created_by"),
)
if data.get("source", False) == "slack":
send_welcome_slack.delay(
str(new_user.id),
True,
f"{new_user.email} was imported to Plane from {service}",
# Check if we need to import users as well
if len(users):
# For all invited users create the users
new_users = User.objects.bulk_create(
[
User(
email=user.get("email").strip().lower(),
username=uuid.uuid4().hex,
password=make_password(uuid.uuid4().hex),
is_password_autoset=True,
)
for user in users
if user.get("import", False) == "invite"
],
batch_size=10,
ignore_conflicts=True,
)
_ = [
send_welcome_slack.delay(
str(user.id),
True,
f"{user.email} was imported to Plane from {service}",
)
for user in new_users
]
def label_sync(data):
existing_label = Label.objects.filter(
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
name__iexact=data.get("name"),
external_id=data.get("external_id", None),
external_source=data.get("external_source"),
)
if not existing_label.exists() and data.get("name"):
label = Label.objects.create(
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
name=data.get("name"),
color=data.get("color"),
created_by_id=data.get("created_by"),
external_id=data.get("external_id", None),
external_source=data.get("external_source"),
)
update_imported_items(data.get("importer_id"), "labels", label.id)
def state_sync(data):
try:
state = State.objects.get(
external_id=data.get("external_id"),
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
)
except State.DoesNotExist:
existing_states = State.objects.filter(
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
group=data.get("state_group"),
name__iexact=data.get("state_name"),
)
if existing_states.exists():
existing_state = existing_states.first()
existing_state.external_id = data.get("external_id")
existing_state.external_source = data.get("external_source")
existing_state.save()
else:
state = State.objects.create(
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
name=data.get("state_name"),
group=data.get("state_group"),
created_by_id=data.get("created_by"),
external_id=data.get("external_id"),
external_source=data.get("external_source"),
workspace_users = User.objects.filter(
email__in=[
user.get("email").strip().lower()
for user in users
if user.get("import", False) == "invite"
or user.get("import", False) == "map"
]
)
update_imported_items(data.get("importer_id"), "states", state.id)
# Check if any of the users are already member of workspace
_ = WorkspaceMember.objects.filter(
member__in=[user for user in workspace_users],
workspace_id=importer.workspace_id,
).update(is_active=True)
def issue_sync(data):
try:
issue = Issue.objects.get(
external_id=data.get("external_id"),
external_source=data.get("external_source"),
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
)
except Issue.DoesNotExist:
# Get the default state
default_state = State.objects.filter(
~Q(name="Triage"), project_id=data.get("project_id"), default=True
).first()
# Add new users to Workspace and project automatically
WorkspaceMember.objects.bulk_create(
[
WorkspaceMember(
member=user,
workspace_id=importer.workspace_id,
created_by=importer.created_by,
)
for user in workspace_users
],
batch_size=100,
ignore_conflicts=True,
)
# 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=data.get("project_id")
ProjectMember.objects.bulk_create(
[
ProjectMember(
project_id=importer.project_id,
workspace_id=importer.workspace_id,
member=user,
created_by=importer.created_by,
)
for user in workspace_users
],
batch_size=100,
ignore_conflicts=True,
)
IssueProperty.objects.bulk_create(
[
IssueProperty(
project_id=importer.project_id,
workspace_id=importer.workspace_id,
user=user,
created_by=importer.created_by,
)
for user in workspace_users
],
batch_size=100,
ignore_conflicts=True,
)
# Check if sync config is on for github importers
if service == "github" and importer.config.get("sync", False):
name = importer.metadata.get("name", False)
url = importer.metadata.get("url", False)
config = importer.metadata.get("config", {})
owner = importer.metadata.get("owner", False)
repository_id = importer.metadata.get("repository_id", False)
workspace_integration = WorkspaceIntegration.objects.get(
workspace_id=importer.workspace_id, integration__provider="github"
)
# Delete the old repository object
GithubRepositorySync.objects.filter(project_id=importer.project_id).delete()
GithubRepository.objects.filter(project_id=importer.project_id).delete()
# Create a Label for github
label = Label.objects.filter(
name="GitHub", project_id=importer.project_id
).first()
# Get the maximum sequence_id
last_id = IssueSequence.objects.filter(
project_id=data.get("project_id")
).aggregate(largest=Max("sequence"))["largest"]
if label is None:
label = Label.objects.create(
name="GitHub",
project_id=importer.project_id,
description="Label to sync Plane issues with GitHub issues",
color="#003773",
)
# Create repository
repo = GithubRepository.objects.create(
name=name,
url=url,
config=config,
repository_id=repository_id,
owner=owner,
project_id=importer.project_id,
)
last_id = 1 if last_id is None else last_id + 1
# Create repo sync
_ = GithubRepositorySync.objects.create(
repository=repo,
workspace_integration=workspace_integration,
actor=workspace_integration.actor,
credentials=importer.data.get("credentials", {}),
project_id=importer.project_id,
label=label,
)
# Get the maximum sort order
largest_sort_order = Issue.objects.filter(
project_id=data.get("project_id"), state=default_state
).aggregate(largest=Max("sort_order"))["largest"]
# Add bot as a member in the project
_ = ProjectMember.objects.get_or_create(
member=workspace_integration.actor,
role=20,
project_id=importer.project_id,
)
largest_sort_order = (
65535 if largest_sort_order is None else largest_sort_order + 10000
)
parent_id = None
if data.get("parent_id", False):
parent_id = Issue.objects.filter(
external_id=data.get("parent_id"),
external_source=data.get("external_source"),
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
).values("id")
if settings.PROXY_BASE_URL:
headers = {"Content-Type": "application/json"}
import_data_json = json.dumps(
ImporterSerializer(importer).data,
cls=DjangoJSONEncoder,
)
_ = requests.post(
f"{settings.PROXY_BASE_URL}/hooks/workspaces/{str(importer.workspace_id)}/projects/{str(importer.project_id)}/importers/{str(service)}/",
json=import_data_json,
headers=headers,
)
# Issues
issue = Issue.objects.create(
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
state_id=get_state_id(data.get("state"), data).get("id")
if get_state_id(data.get("state"), data)
else default_state.id,
name=data.get("name", "Issue Created through Importer")[:255],
description_html=data.get("description_html", "<p></p>"),
sequence_id=last_id,
sort_order=largest_sort_order,
start_date=data.get("start_date", None),
target_date=data.get("target_date", None),
priority=data.get("priority", "none"),
created_by_id=data.get("created_by_id"),
external_id=data.get("external_id"),
external_source=data.get("external_source"),
parent_id=parent_id,
)
# Sequences
_ = IssueSequence.objects.create(
issue=issue,
sequence=issue.sequence_id,
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
)
# Attach Links
_ = IssueLink.objects.create(
issue=issue,
url=data.get("link", {}).get("url", "https://github.com"),
title=data.get("link", {}).get("title", "Original Issue"),
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
created_by_id=data.get("created_by_id"),
)
# Track the issue activities
_ = IssueActivity.objects.create(
issue=issue,
actor_id=data.get("created_by_id"),
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
comment=f"imported the issue from {data.get('external_source')}",
verb="created",
created_by_id=data.get("created_by_id"),
)
update_imported_items(data.get("importer_id"), "issues", issue.id)
def issue_label_sync(data):
issue = Issue.objects.get(
external_source=data.get("external_issue_source"),
external_id=data.get("external_issue_id"),
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
)
if get_label_id(data.get("name"), data):
IssueLabel.objects.create(
issue=issue,
label_id=get_label_id(data.get("name"), data).get("id"),
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
created_by_id=data.get("created_by_id"),
)
def issue_assignee_sync(data):
issue = Issue.objects.get(
external_source=data.get("external_issue_source"),
external_id=data.get("external_issue_id"),
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
)
user = User.objects.filter(email=data.get("email")).values("id")
IssueAssignee.objects.create(
issue=issue,
assignee_id=user,
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
created_by_id=data.get("created_by_id"),
)
def issue_comment_sync(data):
# Create Comments
issue = Issue.objects.get(
external_source=data.get("external_issue_source"),
external_id=data.get("external_issue_id"),
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
)
IssueComment.objects.create(
issue=issue,
comment_html=data.get("comment_html", "<p></p>"),
actor_id=data.get("created_by_id"),
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
created_by_id=get_user_id(data.get("created_by_id")).get("id")
if get_user_id(data.get("created_by_id"))
else data.get("created_by_id"),
external_id=data.get("external_id"),
external_source=data.get("external_source"),
)
def cycles_sync(data):
try:
_ = Cycle.objects.get(
external_id=data.get("external_id"),
external_source=data.get("external_source"),
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
)
except Cycle.DoesNotExist:
cycle = Cycle.objects.create(
name=data.get("name"),
description_html=data.get("description_html", "<p></p>"),
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
created_by_id=data.get("created_by"),
external_id=data.get("external_id"),
external_source=data.get("external_source"),
)
update_imported_items(data.get("importer_id"), "cycles", cycle.id)
def module_sync(data):
try:
_ = Module.objects.get(
external_id=data.get("external_id"),
external_source=data.get("external_source"),
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
)
except Module.DoesNotExist:
module = Module.objects.create(
name=data.get("name"),
description_html=data.get("description_html", "<p></p>"),
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
created_by_id=data.get("created_by"),
external_id=data.get("external_id"),
external_source=data.get("external_source"),
)
update_imported_items(data.get("importer_id"), "modules", module.id)
def modules_issue_sync(data):
module = Module.objects.get(
external_id=data.get("module_id"),
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
external_source=data.get("external_source"),
)
issue = Issue.objects.get(
external_id=data.get("issue_id"),
external_source=data.get("external_source"),
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
)
_ = ModuleIssue.objects.create(
module=module,
issue=issue,
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
created_by_id=data.get("created_by"),
)
def import_sync(data):
importer = Importer.objects.get(pk=data.get("importer_id"))
importer.status = data.get("status")
importer.save(update_fields=["status"])
@shared_task(bind=True, queue="segway_task", max_retries=5)
def import_task(self, data):
type = data.get("type")
if type is None:
return
TYPE_MAPPER = {
"member.sync": members_sync,
"label.sync": label_sync,
"state.sync": state_sync,
"issue.sync": issue_sync,
"issue.label.sync": issue_label_sync,
"issue.assignee.sync": issue_assignee_sync,
"issue.comment.sync": issue_comment_sync,
"cycle.sync": cycles_sync,
"module.sync": module_sync,
"module.issue.sync": modules_issue_sync,
"import.sync": import_sync,
}
try:
func = TYPE_MAPPER.get(type)
if func is None:
return
# Call the function
func(data)
return
except Exception as e:
try:
# Retry with exponential backoff
self.retry(exc=e, countdown=50, backoff=2)
except MaxRetriesExceededError:
# For max retries reached items fail the import
importer = Importer.objects.get(pk=data.get("importer_id"))
importer.status = "failed"
importer.reason = e
importer.save()
importer = Importer.objects.get(pk=importer_id)
importer.status = "failed"
importer.save()
# Print logs if in DEBUG mode
if settings.DEBUG:
print(e)
capture_exception(e)
return
+71 -57
View File
@@ -112,8 +112,16 @@ def track_parent(
epoch,
):
if current_instance.get("parent") != requested_data.get("parent"):
old_parent = Issue.objects.filter(pk=current_instance.get("parent")).first()
new_parent = Issue.objects.filter(pk=requested_data.get("parent")).first()
old_parent = (
Issue.objects.filter(pk=current_instance.get("parent")).first()
if current_instance.get("parent") is not None
else None
)
new_parent = (
Issue.objects.filter(pk=requested_data.get("parent")).first()
if requested_data.get("parent") is not None
else None
)
issue_activities.append(
IssueActivity(
@@ -714,7 +722,9 @@ def create_cycle_issue_activity(
cycle = Cycle.objects.filter(
pk=created_record.get("fields").get("cycle")
).first()
issue = Issue.objects.filter(pk=created_record.get("fields").get("issue")).first()
issue = Issue.objects.filter(
pk=created_record.get("fields").get("issue")
).first()
if issue:
issue.updated_at = timezone.now()
issue.save(update_fields=["updated_at"])
@@ -830,7 +840,9 @@ def create_module_issue_activity(
module = Module.objects.filter(
pk=created_record.get("fields").get("module")
).first()
issue = Issue.objects.filter(pk=created_record.get("fields").get("issue")).first()
issue = Issue.objects.filter(
pk=created_record.get("fields").get("issue")
).first()
if issue:
issue.updated_at = timezone.now()
issue.save(update_fields=["updated_at"])
@@ -1276,40 +1288,42 @@ def create_issue_relation_activity(
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
if current_instance is None and requested_data.get("related_list") is not None:
for issue_relation in requested_data.get("related_list"):
if issue_relation.get("relation_type") == "blocked_by":
relation_type = "blocking"
else:
relation_type = issue_relation.get("relation_type")
issue = Issue.objects.get(pk=issue_relation.get("issue"))
if current_instance is None and requested_data.get("issues") is not None:
for related_issue in requested_data.get("issues"):
issue = Issue.objects.get(pk=related_issue)
issue_activities.append(
IssueActivity(
issue_id=issue_relation.get("related_issue"),
issue_id=issue_id,
actor_id=actor_id,
verb="created",
old_value="",
new_value=f"{issue.project.identifier}-{issue.sequence_id}",
field=relation_type,
field=requested_data.get("relation_type"),
project_id=project_id,
workspace_id=workspace_id,
comment=f"added {relation_type} relation",
old_identifier=issue_relation.get("issue"),
comment=f"added {requested_data.get('relation_type')} relation",
old_identifier=related_issue,
)
)
issue = Issue.objects.get(pk=issue_relation.get("related_issue"))
issue = Issue.objects.get(pk=issue_id)
issue_activities.append(
IssueActivity(
issue_id=issue_relation.get("issue"),
issue_id=related_issue,
actor_id=actor_id,
verb="created",
old_value="",
new_value=f"{issue.project.identifier}-{issue.sequence_id}",
field=f'{issue_relation.get("relation_type")}',
field="blocking"
if requested_data.get("relation_type") == "blocked_by"
else (
"blocked_by"
if requested_data.get("relation_type") == "blocking"
else requested_data.get("relation_type")
),
project_id=project_id,
workspace_id=workspace_id,
comment=f'added {issue_relation.get("relation_type")} relation',
old_identifier=issue_relation.get("related_issue"),
comment=f'added {"blocking" if requested_data.get("relation_type") == "blocked_by" else ("blocked_by" if requested_data.get("relation_type") == "blocking" else requested_data.get("relation_type")),} relation',
old_identifier=issue_id,
epoch=epoch,
)
)
@@ -1329,44 +1343,44 @@ def delete_issue_relation_activity(
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
if current_instance is not None and requested_data.get("related_list") is None:
if current_instance.get("relation_type") == "blocked_by":
relation_type = "blocking"
else:
relation_type = current_instance.get("relation_type")
issue = Issue.objects.get(pk=current_instance.get("issue"))
issue_activities.append(
IssueActivity(
issue_id=current_instance.get("related_issue"),
actor_id=actor_id,
verb="deleted",
old_value=f"{issue.project.identifier}-{issue.sequence_id}",
new_value="",
field=relation_type,
project_id=project_id,
workspace_id=workspace_id,
comment=f"deleted {relation_type} relation",
old_identifier=current_instance.get("issue"),
epoch=epoch,
)
issue = Issue.objects.get(pk=requested_data.get("related_issue"))
issue_activities.append(
IssueActivity(
issue_id=issue_id,
actor_id=actor_id,
verb="deleted",
old_value=f"{issue.project.identifier}-{issue.sequence_id}",
new_value="",
field=requested_data.get("relation_type"),
project_id=project_id,
workspace_id=workspace_id,
comment=f"deleted {requested_data.get('relation_type')} relation",
old_identifier=requested_data.get("related_issue"),
epoch=epoch,
)
issue = Issue.objects.get(pk=current_instance.get("related_issue"))
issue_activities.append(
IssueActivity(
issue_id=current_instance.get("issue"),
actor_id=actor_id,
verb="deleted",
old_value=f"{issue.project.identifier}-{issue.sequence_id}",
new_value="",
field=f'{current_instance.get("relation_type")}',
project_id=project_id,
workspace_id=workspace_id,
comment=f'deleted {current_instance.get("relation_type")} relation',
old_identifier=current_instance.get("related_issue"),
epoch=epoch,
)
)
issue = Issue.objects.get(pk=issue_id)
issue_activities.append(
IssueActivity(
issue_id=requested_data.get("related_issue"),
actor_id=actor_id,
verb="deleted",
old_value=f"{issue.project.identifier}-{issue.sequence_id}",
new_value="",
field="blocking"
if requested_data.get("relation_type") == "blocked_by"
else (
"blocked_by"
if requested_data.get("relation_type") == "blocking"
else requested_data.get("relation_type")
),
project_id=project_id,
workspace_id=workspace_id,
comment=f'deleted {requested_data.get("relation_type")} relation',
old_identifier=requested_data.get("related_issue"),
epoch=epoch,
)
)
def create_draft_issue_activity(
requested_data,
@@ -1460,7 +1474,7 @@ def delete_draft_issue_activity(
# Receive message from room group
@shared_task(queue='internal_tasks')
@shared_task
def issue_activity(
type,
requested_data,
@@ -16,7 +16,7 @@ from plane.db.models import Issue, Project, State
from plane.bgtasks.issue_activites_task import issue_activity
@shared_task(queue='internal_tasks')
@shared_task
def archive_and_close_old_issues():
archive_old_issues()
close_old_issues()
@@ -1,5 +0,0 @@
from celery import shared_task
@shared_task(queue="segway_tasks")
def issue_sync(data):
print(f"Received data from Segway: {data}")
@@ -17,7 +17,7 @@ from sentry_sdk import capture_exception
from plane.license.utils.instance_value import get_email_configuration
@shared_task(queue='internal_tasks')
@shared_task
def magic_link(email, key, token, current_site):
try:
(
+5 -2
View File
@@ -183,7 +183,7 @@ def createMentionNotification(project, notification_comment, issue, actor_id, me
)
@shared_task(queue='internal_tasks')
@shared_task
def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activities_created, requested_data, current_instance):
issue_activities_created = (
json.loads(
@@ -291,6 +291,9 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi
sender = "in_app:issue_activities:assigned"
for issue_activity in issue_activities_created:
# Do not send notification for description update
if issue_activity.get("field") == "description":
continue;
issue_comment = issue_activity.get("issue_comment")
if issue_comment is not None:
issue_comment = IssueComment.objects.get(
@@ -341,7 +344,7 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi
.order_by("-created_at")
.first()
)
actor = User.objects.get(pk=actor_id)
for mention_id in comment_mentions:
@@ -15,7 +15,7 @@ from sentry_sdk import capture_exception
from plane.db.models import Project, User, ProjectMemberInvite
from plane.license.utils.instance_value import get_email_configuration
@shared_task(queue='internal_tasks')
@shared_task
def project_invitation(email, project_id, token, current_site, invitor):
try:
user = User.objects.get(email=invitor)
+1 -1
View File
@@ -11,7 +11,7 @@ from slack_sdk.errors import SlackApiError
from plane.db.models import User
@shared_task(queue='internal_tasks')
@shared_task
def send_welcome_slack(user_id, created, message):
try:
instance = User.objects.get(pk=user_id)
+1 -2
View File
@@ -71,7 +71,6 @@ def get_model_data(event, event_id, many=False):
retry_backoff=600,
max_retries=5,
retry_jitter=True,
queue='internal_tasks'
)
def webhook_task(self, webhook, slug, event, event_data, action):
try:
@@ -162,7 +161,7 @@ def webhook_task(self, webhook, slug, event, event_data, action):
return
@shared_task(queue='internal_tasks')
@shared_task()
def send_webhook(event, payload, kw, action, slug, bulk):
try:
webhooks = Webhook.objects.filter(workspace__slug=slug, is_active=True)
@@ -20,7 +20,7 @@ from plane.db.models import Workspace, WorkspaceMemberInvite, User
from plane.license.utils.instance_value import get_email_configuration
@shared_task(queue='internal_tasks')
@shared_task
def workspace_invitation(email, workspace_id, token, current_site, invitor):
try:
user = User.objects.get(email=invitor)
+1 -1
View File
@@ -8,7 +8,7 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production")
ri = redis_instance()
app = Celery('tasks', broker='pyamqp://guest:guest@localhost:5672//')
app = Celery("plane")
# Using a string here means the worker will not have to
# pickle the object when using Windows.
@@ -1,4 +1,4 @@
# Generated by Django 4.2.7 on 2023-12-20 14:33
# Generated by Django 4.2.7 on 2023-12-29 10:20
from django.db import migrations, models
@@ -18,12 +18,17 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='cycle',
name='external_source',
field=models.CharField(blank=True, null=True),
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='importer',
name='reason',
field=models.TextField(blank=True),
model_name='inboxissue',
name='external_id',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='inboxissue',
name='external_source',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='issue',
@@ -33,7 +38,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='issue',
name='external_source',
field=models.CharField(blank=True, null=True),
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='issuecomment',
@@ -43,7 +48,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='issuecomment',
name='external_source',
field=models.CharField(blank=True, null=True),
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='label',
@@ -53,7 +58,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='label',
name='external_source',
field=models.CharField(blank=True, null=True),
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='module',
@@ -63,7 +68,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='module',
name='external_source',
field=models.CharField(blank=True, null=True),
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='state',
@@ -73,6 +78,6 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='state',
name='external_source',
field=models.CharField(blank=True, null=True),
field=models.CharField(blank=True, max_length=255, null=True),
),
]
@@ -0,0 +1,136 @@
# Generated by Django 4.2.7 on 2023-12-20 11:14
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import plane.db.models.cycle
import plane.db.models.issue
import plane.db.models.module
import plane.db.models.view
import plane.db.models.workspace
import uuid
class Migration(migrations.Migration):
dependencies = [
('db', '0051_cycle_external_id_cycle_external_source_and_more'),
]
operations = [
migrations.RenameField(
model_name='issueview',
old_name='query_data',
new_name='filters',
),
migrations.RenameField(
model_name='issueproperty',
old_name='properties',
new_name='display_properties',
),
migrations.AlterField(
model_name='issueproperty',
name='display_properties',
field=models.JSONField(default=plane.db.models.issue.get_default_display_properties),
),
migrations.AddField(
model_name='issueproperty',
name='display_filters',
field=models.JSONField(default=plane.db.models.issue.get_default_display_filters),
),
migrations.AddField(
model_name='issueproperty',
name='filters',
field=models.JSONField(default=plane.db.models.issue.get_default_filters),
),
migrations.AddField(
model_name='issueview',
name='display_filters',
field=models.JSONField(default=plane.db.models.view.get_default_display_filters),
),
migrations.AddField(
model_name='issueview',
name='display_properties',
field=models.JSONField(default=plane.db.models.view.get_default_display_properties),
),
migrations.AddField(
model_name='issueview',
name='sort_order',
field=models.FloatField(default=65535),
),
migrations.AlterField(
model_name='issueview',
name='project',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.CreateModel(
name='WorkspaceUserProperties',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('filters', models.JSONField(default=plane.db.models.workspace.get_default_filters)),
('display_filters', models.JSONField(default=plane.db.models.workspace.get_default_display_filters)),
('display_properties', models.JSONField(default=plane.db.models.workspace.get_default_display_properties)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_user_properties', to=settings.AUTH_USER_MODEL)),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_user_properties', to='db.workspace')),
],
options={
'verbose_name': 'Workspace User Property',
'verbose_name_plural': 'Workspace User Property',
'db_table': 'Workspace_user_properties',
'ordering': ('-created_at',),
'unique_together': {('workspace', 'user')},
},
),
migrations.CreateModel(
name='ModuleUserProperties',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('filters', models.JSONField(default=plane.db.models.module.get_default_filters)),
('display_filters', models.JSONField(default=plane.db.models.module.get_default_display_filters)),
('display_properties', models.JSONField(default=plane.db.models.module.get_default_display_properties)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('module', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='module_user_properties', to='db.module')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='module_user_properties', to=settings.AUTH_USER_MODEL)),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')),
],
options={
'verbose_name': 'Module User Property',
'verbose_name_plural': 'Module User Property',
'db_table': 'module_user_properties',
'ordering': ('-created_at',),
'unique_together': {('module', 'user')},
},
),
migrations.CreateModel(
name='CycleUserProperties',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('filters', models.JSONField(default=plane.db.models.cycle.get_default_filters)),
('display_filters', models.JSONField(default=plane.db.models.cycle.get_default_display_filters)),
('display_properties', models.JSONField(default=plane.db.models.cycle.get_default_display_properties)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('cycle', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cycle_user_properties', to='db.cycle')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cycle_user_properties', to=settings.AUTH_USER_MODEL)),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')),
],
options={
'verbose_name': 'Cycle User Property',
'verbose_name_plural': 'Cycle User Properties',
'db_table': 'cycle_user_properties',
'ordering': ('-created_at',),
'unique_together': {('cycle', 'user')},
},
),
]
@@ -0,0 +1,65 @@
# Generated by Django 4.2.7 on 2024-01-02 13:15
from plane.db.models import WorkspaceUserProperties, ProjectMember, IssueView
from django.db import migrations
def workspace_user_properties(apps, schema_editor):
WorkspaceMember = apps.get_model("db", "WorkspaceMember")
updated_workspace_user_properties = []
for workspace_members in WorkspaceMember.objects.all():
updated_workspace_user_properties.append(
WorkspaceUserProperties(
user_id=workspace_members.member_id,
display_filters=workspace_members.view_props.get("display_filters"),
display_properties=workspace_members.view_props.get("display_properties"),
workspace_id=workspace_members.workspace_id,
)
)
WorkspaceUserProperties.objects.bulk_create(updated_workspace_user_properties, batch_size=2000)
def project_user_properties(apps, schema_editor):
IssueProperty = apps.get_model("db", "IssueProperty")
updated_issue_user_properties = []
for issue_property in IssueProperty.objects.all():
project_member = ProjectMember.objects.filter(project_id=issue_property.project_id, member_id=issue_property.user_id).first()
if project_member:
issue_property.filters = project_member.view_props.get("filters")
issue_property.display_filters = project_member.view_props.get("display_filters")
updated_issue_user_properties.append(issue_property)
IssueProperty.objects.bulk_update(updated_issue_user_properties, ["filters", "display_filters"], batch_size=2000)
def issue_view(apps, schema_editor):
GlobalView = apps.get_model("db", "GlobalView")
updated_issue_views = []
for global_view in GlobalView.objects.all():
updated_issue_views.append(
IssueView(
workspace_id=global_view.workspace_id,
name=global_view.name,
description=global_view.description,
query=global_view.query,
access=global_view.access,
filters=global_view.query_data.get("filters", {}),
sort_order=global_view.sort_order,
created_by_id=global_view.created_by_id,
updated_by_id=global_view.updated_by_id,
)
)
IssueView.objects.bulk_create(updated_issue_views, batch_size=100)
class Migration(migrations.Migration):
dependencies = [
('db', '0052_auto_20231220_1141'),
]
operations = [
migrations.RunPython(workspace_user_properties),
migrations.RunPython(project_user_properties),
migrations.RunPython(issue_view),
]
+4 -2
View File
@@ -9,6 +9,8 @@ from .workspace import (
WorkspaceMemberInvite,
TeamMember,
WorkspaceTheme,
WorkspaceUserProperties,
WorkspaceBaseModel,
)
from .project import (
@@ -48,11 +50,11 @@ from .social_connection import SocialLoginConnection
from .state import State
from .cycle import Cycle, CycleIssue, CycleFavorite
from .cycle import Cycle, CycleIssue, CycleFavorite, CycleUserProperties
from .view import GlobalView, IssueView, IssueViewFavorite
from .module import Module, ModuleMember, ModuleIssue, ModuleLink, ModuleFavorite
from .module import Module, ModuleMember, ModuleIssue, ModuleLink, ModuleFavorite, ModuleUserProperties
from .api import APIToken, APIActivityLog
+2
View File
@@ -22,6 +22,8 @@ class BaseModel(AuditModel):
user = get_current_user()
if user is None or user.is_anonymous:
self.created_by = None
self.updated_by = None
super(BaseModel, self).save(*args, **kwargs)
else:
# Check if the model is being created or updated
+70 -4
View File
@@ -6,6 +6,47 @@ from django.conf import settings
from . import ProjectBaseModel
def get_default_filters():
return {
"priority": None,
"state": None,
"state_group": None,
"assignees": None,
"created_by": None,
"labels": None,
"start_date": None,
"target_date": None,
"subscriber": None,
}
def get_default_display_filters():
return {
"group_by": None,
"order_by": "-created_at",
"type": None,
"sub_issue": True,
"show_empty_groups": True,
"layout": "list",
"calendar_date_range": "",
}
def get_default_display_properties():
return {
"assignee": True,
"attachment_count": True,
"created_on": True,
"due_date": True,
"estimate": True,
"key": True,
"labels": True,
"link": True,
"priority": True,
"start_date": True,
"state": True,
"sub_issue_count": True,
"updated_on": True,
}
class Cycle(ProjectBaseModel):
name = models.CharField(max_length=255, verbose_name="Cycle Name")
description = models.TextField(verbose_name="Cycle Description", blank=True)
@@ -18,7 +59,7 @@ class Cycle(ProjectBaseModel):
)
view_props = models.JSONField(default=dict)
sort_order = models.FloatField(default=65535)
external_source = models.CharField(null=True, blank=True)
external_source = models.CharField(max_length=255, null=True, blank=True)
external_id = models.CharField(max_length=255, blank=True, null=True)
class Meta:
@@ -29,9 +70,9 @@ class Cycle(ProjectBaseModel):
def save(self, *args, **kwargs):
if self._state.adding:
smallest_sort_order = Cycle.objects.filter(project=self.project).aggregate(
smallest=models.Min("sort_order")
)["smallest"]
smallest_sort_order = Cycle.objects.filter(
project=self.project
).aggregate(smallest=models.Min("sort_order"))["smallest"]
if smallest_sort_order is not None:
self.sort_order = smallest_sort_order - 10000
@@ -89,3 +130,28 @@ class CycleFavorite(ProjectBaseModel):
def __str__(self):
"""Return user and the cycle"""
return f"{self.user.email} <{self.cycle.name}>"
class CycleUserProperties(ProjectBaseModel):
cycle = models.ForeignKey(
"db.Cycle", on_delete=models.CASCADE, related_name="cycle_user_properties"
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="cycle_user_properties",
)
filters = models.JSONField(default=get_default_filters)
display_filters = models.JSONField(default=get_default_display_filters)
display_properties = models.JSONField(default=get_default_display_properties)
class Meta:
unique_together = ["cycle", "user"]
verbose_name = "Cycle User Property"
verbose_name_plural = "Cycle User Properties"
db_table = "cycle_user_properties"
ordering = ("-created_at",)
def __str__(self):
return f"{self.cycle.name} {self.user.email}"
-1
View File
@@ -34,7 +34,6 @@ class Importer(ProjectBaseModel):
"db.APIToken", on_delete=models.CASCADE, related_name="importer"
)
imported_data = models.JSONField(null=True)
reason = models.TextField(blank=True)
class Meta:
verbose_name = "Importer"
+2
View File
@@ -39,6 +39,8 @@ class InboxIssue(ProjectBaseModel):
"db.Issue", related_name="inbox_duplicate", on_delete=models.SET_NULL, null=True
)
source = models.TextField(blank=True, null=True)
external_source = models.CharField(max_length=255, null=True, blank=True)
external_id = models.CharField(max_length=255, blank=True, null=True)
class Meta:
verbose_name = "InboxIssue"
+52 -9
View File
@@ -33,6 +33,48 @@ def get_default_properties():
}
def get_default_filters():
return {
"priority": None,
"state": None,
"state_group": None,
"assignees": None,
"created_by": None,
"labels": None,
"start_date": None,
"target_date": None,
"subscriber": None,
}
def get_default_display_filters():
return {
"group_by": None,
"order_by": "-created_at",
"type": None,
"sub_issue": True,
"show_empty_groups": True,
"layout": "list",
"calendar_date_range": "",
}
def get_default_display_properties():
return {
"assignee": True,
"attachment_count": True,
"created_on": True,
"due_date": True,
"estimate": True,
"key": True,
"labels": True,
"link": True,
"priority": True,
"start_date": True,
"state": True,
"sub_issue_count": True,
"updated_on": True,
}
# TODO: Handle identifiers for Bulk Inserts - nk
class IssueManager(models.Manager):
def get_queryset(self):
@@ -102,7 +144,7 @@ class Issue(ProjectBaseModel):
completed_at = models.DateTimeField(null=True)
archived_at = models.DateField(null=True)
is_draft = models.BooleanField(default=False)
external_source = models.CharField(null=True, blank=True)
external_source = models.CharField(max_length=255, null=True, blank=True)
external_id = models.CharField(max_length=255, blank=True, null=True)
objects = models.Manager()
@@ -134,6 +176,7 @@ class Issue(ProjectBaseModel):
except ImportError:
pass
if self._state.adding:
# Get the maximum display_id value from the database
last_id = IssueSequence.objects.filter(project=self.project).aggregate(
@@ -211,9 +254,8 @@ class IssueRelation(ProjectBaseModel):
ordering = ("-created_at",)
def __str__(self):
return f"{self.issue.name} {self.related_issue.name}"
return f"{self.issue.name} {self.related_issue.name}"
class IssueMention(ProjectBaseModel):
issue = models.ForeignKey(
Issue, on_delete=models.CASCADE, related_name="issue_mention"
@@ -223,7 +265,6 @@ class IssueMention(ProjectBaseModel):
on_delete=models.CASCADE,
related_name="issue_mention",
)
class Meta:
unique_together = ["issue", "mention"]
verbose_name = "Issue Mention"
@@ -232,7 +273,7 @@ class IssueMention(ProjectBaseModel):
ordering = ("-created_at",)
def __str__(self):
return f"{self.issue.name} {self.mention.email}"
return f"{self.issue.name} {self.mention.email}"
class IssueAssignee(ProjectBaseModel):
@@ -369,7 +410,7 @@ class IssueComment(ProjectBaseModel):
default="INTERNAL",
max_length=100,
)
external_source = models.CharField(null=True, blank=True)
external_source = models.CharField(max_length=255, null=True, blank=True)
external_id = models.CharField(max_length=255, blank=True, null=True)
def save(self, *args, **kwargs):
@@ -395,7 +436,9 @@ class IssueProperty(ProjectBaseModel):
on_delete=models.CASCADE,
related_name="issue_property_user",
)
properties = models.JSONField(default=get_default_properties)
filters = models.JSONField(default=get_default_filters)
display_filters = models.JSONField(default=get_default_display_filters)
display_properties = models.JSONField(default=get_default_display_properties)
class Meta:
verbose_name = "Issue Property"
@@ -421,7 +464,7 @@ class Label(ProjectBaseModel):
description = models.TextField(blank=True)
color = models.CharField(max_length=255, blank=True)
sort_order = models.FloatField(default=65535)
external_source = models.CharField(null=True, blank=True)
external_source = models.CharField(max_length=255, null=True, blank=True)
external_id = models.CharField(max_length=255, blank=True, null=True)
class Meta:
+67 -1
View File
@@ -6,6 +6,47 @@ from django.conf import settings
from . import ProjectBaseModel
def get_default_filters():
return {
"priority": None,
"state": None,
"state_group": None,
"assignees": None,
"created_by": None,
"labels": None,
"start_date": None,
"target_date": None,
"subscriber": None,
},
def get_default_display_filters():
return {
"group_by": None,
"order_by": "-created_at",
"type": None,
"sub_issue": True,
"show_empty_groups": True,
"layout": "list",
"calendar_date_range": "",
}
def get_default_display_properties():
return {
"assignee": True,
"attachment_count": True,
"created_on": True,
"due_date": True,
"estimate": True,
"key": True,
"labels": True,
"link": True,
"priority": True,
"start_date": True,
"state": True,
"sub_issue_count": True,
"updated_on": True,
}
class Module(ProjectBaseModel):
name = models.CharField(max_length=255, verbose_name="Module Name")
description = models.TextField(verbose_name="Module Description", blank=True)
@@ -41,7 +82,7 @@ class Module(ProjectBaseModel):
)
view_props = models.JSONField(default=dict)
sort_order = models.FloatField(default=65535)
external_source = models.CharField(null=True, blank=True)
external_source = models.CharField(max_length=255, null=True, blank=True)
external_id = models.CharField(max_length=255, blank=True, null=True)
class Meta:
@@ -141,3 +182,28 @@ class ModuleFavorite(ProjectBaseModel):
def __str__(self):
"""Return user and the module"""
return f"{self.user.email} <{self.module.name}>"
class ModuleUserProperties(ProjectBaseModel):
module = models.ForeignKey(
"db.Module", on_delete=models.CASCADE, related_name="module_user_properties"
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="module_user_properties",
)
filters = models.JSONField(default=get_default_filters)
display_filters = models.JSONField(default=get_default_display_filters)
display_properties = models.JSONField(default=get_default_display_properties)
class Meta:
unique_together = ["module", "user"]
verbose_name = "Module User Property"
verbose_name_plural = "Module User Property"
db_table = "module_user_properties"
ordering = ("-created_at",)
def __str__(self):
return f"{self.module.name} {self.user.email}"
+1 -1
View File
@@ -24,7 +24,7 @@ class State(ProjectBaseModel):
max_length=20,
)
default = models.BooleanField(default=False)
external_source = models.CharField(null=True, blank=True)
external_source = models.CharField(max_length=255, null=True, blank=True)
external_id = models.CharField(max_length=255, blank=True, null=True)
def __str__(self):
+47 -3
View File
@@ -3,9 +3,50 @@ from django.db import models
from django.conf import settings
# Module import
from . import ProjectBaseModel, BaseModel
from . import ProjectBaseModel, BaseModel, WorkspaceBaseModel
def get_default_filters():
return {
"priority": None,
"state": None,
"state_group": None,
"assignees": None,
"created_by": None,
"labels": None,
"start_date": None,
"target_date": None,
"subscriber": None,
}
def get_default_display_filters():
return {
"group_by": None,
"order_by": "-created_at",
"type": None,
"sub_issue": True,
"show_empty_groups": True,
"layout": "list",
"calendar_date_range": "",
}
def get_default_display_properties():
return {
"assignee": True,
"attachment_count": True,
"created_on": True,
"due_date": True,
"estimate": True,
"key": True,
"labels": True,
"link": True,
"priority": True,
"start_date": True,
"state": True,
"sub_issue_count": True,
"updated_on": True,
}
class GlobalView(BaseModel):
workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="global_views"
@@ -40,14 +81,17 @@ class GlobalView(BaseModel):
return f"{self.name} <{self.workspace.name}>"
class IssueView(ProjectBaseModel):
class IssueView(WorkspaceBaseModel):
name = models.CharField(max_length=255, verbose_name="View Name")
description = models.TextField(verbose_name="View Description", blank=True)
query = models.JSONField(verbose_name="View Query")
filters = models.JSONField(default=dict)
display_filters = models.JSONField(default=get_default_display_filters)
display_properties = models.JSONField(default=get_default_display_properties)
access = models.PositiveSmallIntegerField(
default=1, choices=((0, "Private"), (1, "Public"))
)
query_data = models.JSONField(default=dict)
sort_order = models.FloatField(default=65535)
class Meta:
verbose_name = "Issue View"
+86
View File
@@ -54,6 +54,51 @@ def get_default_props():
},
}
def get_default_filters():
return {
"priority": None,
"state": None,
"state_group": None,
"assignees": None,
"created_by": None,
"labels": None,
"start_date": None,
"target_date": None,
"subscriber": None,
}
def get_default_display_filters():
return {
"display_filters": {
"group_by": None,
"order_by": "-created_at",
"type": None,
"sub_issue": True,
"show_empty_groups": True,
"layout": "list",
"calendar_date_range": "",
},
}
def get_default_display_properties():
return {
"display_properties": {
"assignee": True,
"attachment_count": True,
"created_on": True,
"due_date": True,
"estimate": True,
"key": True,
"labels": True,
"link": True,
"priority": True,
"start_date": True,
"state": True,
"sub_issue_count": True,
"updated_on": True,
},
}
def get_issue_props():
return {
@@ -103,6 +148,22 @@ class Workspace(BaseModel):
ordering = ("-created_at",)
class WorkspaceBaseModel(BaseModel):
workspace = models.ForeignKey(
"db.Workspace", models.CASCADE, related_name="workspace_%(class)s"
)
project = models.ForeignKey(
"db.Project", models.CASCADE, related_name="project_%(class)s", null=True
)
class Meta:
abstract = True
def save(self, *args, **kwargs):
if self.project:
self.workspace = self.project.workspace
super(WorkspaceBaseModel, self).save(*args, **kwargs)
class WorkspaceMember(BaseModel):
workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="workspace_member"
@@ -218,3 +279,28 @@ class WorkspaceTheme(BaseModel):
verbose_name_plural = "Workspace Themes"
db_table = "workspace_themes"
ordering = ("-created_at",)
class WorkspaceUserProperties(BaseModel):
workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="workspace_user_properties"
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="workspace_user_properties",
)
filters = models.JSONField(default=get_default_filters)
display_filters = models.JSONField(default=get_default_display_filters)
display_properties = models.JSONField(default=get_default_display_properties)
class Meta:
unique_together = ["workspace", "user"]
verbose_name = "Workspace User Property"
verbose_name_plural = "Workspace User Property"
db_table = "Workspace_user_properties"
ordering = ("-created_at",)
def __str__(self):
return f"{self.workspace.name} {self.user.email}"
+16 -29
View File
@@ -5,7 +5,6 @@ import ssl
import certifi
from datetime import timedelta
from urllib.parse import urlparse
from kombu import Exchange, Queue
# Django imports
from django.core.management.utils import get_random_secret_key
@@ -149,9 +148,6 @@ else:
REDIS_URL = os.environ.get("REDIS_URL")
REDIS_SSL = REDIS_URL and "rediss" in REDIS_URL
# RabbitMq Config
RABBITMQ_URL = os.environ.get("RABBITMQ_URL")
if REDIS_SSL:
CACHES = {
"default": {
@@ -274,28 +270,18 @@ SIMPLE_JWT = {
# Celery Configuration
CELERY_TIMEZONE = TIME_ZONE
CELERY_TASK_SERIALIZER = "json"
CELERY_ACCEPT_CONTENT = ["json"]
CELERY_ACCEPT_CONTENT = ["application/json"]
CELERY_BROKER_URL = RABBITMQ_URL
CELERY_RESULT_BACKEND = REDIS_URL
CELERY_QUEUES = (
Queue(
"internal_tasks",
Exchange("internal_exchange", type="direct"),
routing_key="internal",
),
Queue(
"external_tasks",
Exchange("external_exchange", type="direct"),
routing_key="external",
),
Queue(
"segway_tasks",
Exchange("segway_exchange", type="direct"),
routing_key="segway",
),
)
if REDIS_SSL:
redis_url = os.environ.get("REDIS_URL")
broker_url = (
f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
)
CELERY_BROKER_URL = broker_url
CELERY_RESULT_BACKEND = broker_url
else:
CELERY_BROKER_URL = REDIS_URL
CELERY_RESULT_BACKEND = REDIS_URL
CELERY_IMPORTS = (
"plane.bgtasks.issue_automation_task",
@@ -343,11 +329,12 @@ USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1
POSTHOG_API_KEY = os.environ.get("POSTHOG_API_KEY", False)
POSTHOG_HOST = os.environ.get("POSTHOG_HOST", False)
# instance key
INSTANCE_KEY = os.environ.get(
"INSTANCE_KEY", "ae6517d563dfc13d8270bd45cf17b08f70b37d989128a9dab46ff687603333c3"
)
# Skip environment variable configuration
SKIP_ENV_VAR = os.environ.get("SKIP_ENV_VAR", "1") == "1"
DATA_UPLOAD_MAX_MEMORY_SIZE = int(os.environ.get("FILE_SIZE_LIMIT", 5242880))
# Segway
SEGWAY_BASE_URL = os.environ.get("SEGWAY_BASE_URL", "http://localhost:9000")
SEGWAY_KEY = os.environ.get("SEGWAY_KEY", False)
+1 -1
View File
@@ -30,7 +30,7 @@ openpyxl==3.1.2
beautifulsoup4==4.12.2
dj-database-url==2.1.0
posthog==3.0.2
cryptography==41.0.6
cryptography==41.0.5
lxml==4.9.3
boto3==1.28.40
+1 -1
View File
@@ -39,7 +39,7 @@ function download(){
echo ""
echo "Latest version is now available for you to use"
echo ""
echo "In case of Upgrade, your new setting file is available as 'variables-upgrade.env'. Please compare and set the required values in '.env 'file."
echo "In case of Upgrade, your new setting file is availabe as 'variables-upgrade.env'. Please compare and set the required values in '.env 'file."
echo ""
}
+3
View File
@@ -18,6 +18,9 @@ http {
location / {
proxy_pass http://web:3000/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /api/ {
+3 -2
View File
@@ -10,7 +10,8 @@
"packages/eslint-config-custom",
"packages/tailwind-config-custom",
"packages/tsconfig",
"packages/ui"
"packages/ui",
"packages/types"
],
"scripts": {
"build": "turbo run build",
@@ -27,7 +28,7 @@
"prettier": "latest",
"prettier-plugin-tailwindcss": "^0.5.4",
"tailwindcss": "^3.3.3",
"turbo": "^1.11.1"
"turbo": "^1.11.2"
},
"resolutions": {
"@types/react": "18.2.42"
@@ -52,12 +52,12 @@ export const CoreEditorExtensions = (
class: "leading-normal -mb-2",
},
},
// blockquote: {
// HTMLAttributes: {
// class: "border-l-4 border-custom-border-300",
// },
// },
code: false,
code: {
HTMLAttributes: {
class: "rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000",
spellcheck: "false",
},
},
codeBlock: false,
horizontalRule: false,
dropcursor: {
+1 -2
View File
@@ -35,7 +35,6 @@
"@tiptap/extension-placeholder": "^2.1.13",
"@tiptap/pm": "^2.1.13",
"@tiptap/suggestion": "^2.1.13",
"eslint": "8.36.0",
"eslint-config-next": "13.2.4",
"react-popper": "^2.3.0",
"tippy.js": "^6.3.7",
@@ -45,7 +44,7 @@
"@types/node": "18.15.3",
"@types/react": "^18.2.42",
"@types/react-dom": "^18.2.17",
"eslint": "^7.32.0",
"eslint": "8.36.0",
"postcss": "^8.4.29",
"tailwind-config-custom": "*",
"tsconfig": "*",
+1 -2
View File
@@ -33,7 +33,6 @@
"@tiptap/pm": "^2.1.13",
"@tiptap/react": "^2.1.13",
"@tiptap/suggestion": "^2.1.13",
"eslint": "8.36.0",
"eslint-config-next": "13.2.4",
"lucide-react": "^0.294.0",
"tippy.js": "^6.3.7"
@@ -42,7 +41,7 @@
"@types/node": "18.15.3",
"@types/react": "^18.2.42",
"@types/react-dom": "^18.2.17",
"eslint": "^7.32.0",
"eslint": "8.36.0",
"postcss": "^8.4.29",
"tailwind-config-custom": "*",
"tsconfig": "*",
-17
View File
@@ -1,17 +0,0 @@
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"outDir": "./dist",
"baseUrl": "./src",
"moduleResolution": "node",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}
+7
View File
@@ -0,0 +1,7 @@
{
"name": "@plane/types",
"version": "0.14.0",
"private": true,
"main": "./src/index.d.ts"
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { IProjectLite, IWorkspaceLite } from "types";
import { IProjectLite, IWorkspaceLite } from "@plane/types";
export interface IGptResponse {
response: string;
+1 -3
View File
@@ -1,6 +1,4 @@
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode;
};
export interface IAppConfig {
email_password_login: boolean;
+6 -3
View File
@@ -1,7 +1,9 @@
import type { IUser, IIssue, IProjectLite, IWorkspaceLite, IIssueFilterOptions, IUserLite } from "types";
import type { IUser, TIssue, IProjectLite, IWorkspaceLite, IIssueFilterOptions, IUserLite } from "@plane/types";
export type TCycleView = "all" | "active" | "upcoming" | "completed" | "draft";
export type TCycleGroups = "current" | "upcoming" | "completed" | "draft";
export type TCycleLayout = "list" | "board" | "gantt";
export interface ICycle {
@@ -24,6 +26,7 @@ export interface ICycle {
owned_by: IUser;
project: string;
project_detail: IProjectLite;
status: TCycleGroups;
sort_order: number;
start_date: string | null;
started_issues: number;
@@ -65,7 +68,7 @@ export type TLabelsDistribution = {
export interface CycleIssueResponse {
id: string;
issue_detail: IIssue;
issue_detail: TIssue;
created_at: Date;
updated_at: Date;
created_by: string;
@@ -79,7 +82,7 @@ export interface CycleIssueResponse {
export type SelectCycleType = (ICycle & { actionType: "edit" | "delete" | "create-issue" }) | undefined;
export type SelectIssue = (IIssue & { actionType: "edit" | "delete" | "create" }) | null;
export type SelectIssue = (TIssue & { actionType: "edit" | "delete" | "create" }) | null;
export type CycleDateCheckData = {
start_date: string;
@@ -1,24 +1,24 @@
export interface IEstimate {
id: string;
created_at: Date;
updated_at: Date;
name: string;
description: string;
created_by: string;
updated_by: string;
points: IEstimatePoint[];
description: string;
id: string;
name: string;
project: string;
project_detail: IProject;
updated_at: Date;
updated_by: string;
points: IEstimatePoint[];
workspace: string;
workspace_detail: IWorkspace;
}
export interface IEstimatePoint {
id: string;
created_at: string;
created_by: string;
description: string;
estimate: string;
id: string;
key: number;
project: string;
updated_at: string;
@@ -1,9 +1,9 @@
export * from "./github-importer";
export * from "./jira-importer";
import { IProjectLite } from "types/projects";
import { IProjectLite } from "../projects";
// types
import { IUserLite } from "types/users";
import { IUserLite } from "../users";
export interface IImporterService {
created_at: string;
+2 -2
View File
@@ -1,7 +1,7 @@
import { IIssue } from "./issues";
import { TIssue } from "./issues";
import type { IProjectLite } from "./projects";
export interface IInboxIssue extends IIssue {
export interface IInboxIssue extends TIssue {
issue_inbox: {
duplicate_to: string | null;
id: string;
@@ -21,6 +21,11 @@ export * from "./reaction";
export * from "./view-props";
export * from "./workspace-views";
export * from "./webhook";
export * from "./issues/base"; // TODO: Remove this after development and the refactor/mobx-store-issue branch is stable
export * from "./auth";
export * from "./api_token";
export * from "./instance";
export * from "./app";
export type NestedKeyOf<ObjectType extends object> = {
[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
+93 -5
View File
@@ -1,3 +1,4 @@
import { ReactElement } from "react";
import { KeyedMutator } from "swr";
import type {
IState,
@@ -10,7 +11,8 @@ import type {
IStateLite,
Properties,
IIssueDisplayFilterOptions,
} from "types";
IIssueReaction,
} from "@plane/types";
export interface IIssueCycle {
id: string;
@@ -83,6 +85,7 @@ export interface IIssue {
attachment_count: number;
attachments: any[];
issue_relations: IssueRelation[];
issue_reactions: IIssueReaction[];
related_issues: IssueRelation[];
bridge_id?: string | null;
completed_at: Date;
@@ -138,7 +141,7 @@ export interface ISubIssuesState {
export interface ISubIssueResponse {
state_distribution: ISubIssuesState;
sub_issues: IIssue[];
sub_issues: TIssue[];
}
export interface BlockeIssueDetail {
@@ -240,13 +243,13 @@ export interface IIssueAttachment {
}
export interface IIssueViewProps {
groupedIssues: { [key: string]: IIssue[] } | undefined;
groupedIssues: { [key: string]: TIssue[] } | undefined;
displayFilters: IIssueDisplayFilterOptions | undefined;
isEmpty: boolean;
mutateIssues: KeyedMutator<
| IIssue[]
| TIssue[]
| {
[key: string]: IIssue[];
[key: string]: TIssue[];
}
>;
params: any;
@@ -254,3 +257,88 @@ export interface IIssueViewProps {
}
export type TIssuePriorities = "urgent" | "high" | "medium" | "low" | "none";
export interface ViewFlags {
enableQuickAdd: boolean;
enableIssueCreation: boolean;
enableInlineEditing: boolean;
}
export type GroupByColumnTypes =
| "project"
| "state"
| "state_detail.group"
| "priority"
| "labels"
| "assignees"
| "created_by";
export interface IGroupByColumn {
id: string;
name: string;
Icon: ReactElement | undefined;
payload: Partial<TIssue>;
}
export interface IIssueMap {
[key: string]: TIssue;
}
// new issue structure types
export type TIssue = {
id: string;
name: string;
state_id: string;
description_html: string;
sort_order: number;
completed_at: string | null;
estimate_point: number | null;
priority: TIssuePriorities;
start_date: string | null;
target_date: string | null;
sequence_id: number;
project_id: string;
parent_id: string | null;
cycle_id: string | null;
module_id: string | null;
label_ids: string[];
assignee_ids: string[];
sub_issues_count: number;
created_at: string;
updated_at: string;
created_by: string;
updated_by: string;
attachment_count: number;
link_count: number;
is_subscribed: boolean;
archived_at: boolean;
is_draft: boolean;
// tempId is used for optimistic updates. It is not a part of the API response.
tempId?: string;
// issue details
related_issues: any;
issue_reactions: any;
issue_relations: any;
issue_cycle: any;
issue_module: any;
parent_detail: any;
issue_link: any;
};
export type TIssueMap = {
[issue_id: string]: TIssue;
};
export type TLoader = "init-loader" | "mutation" | undefined;
export type TGroupedIssues = {
[group_id: string]: string[];
};
export type TSubGroupedIssues = {
[sub_grouped_id: string]: {
[group_id: string]: string[];
};
};
export type TUnGroupedIssues = string[];
+23
View File
@@ -0,0 +1,23 @@
// issues
export * from "./issue";
export * from "./issue_reaction";
export * from "./issue_link";
export * from "./issue_attachment";
export * from "./issue_relation";
export * from "./issue_activity";
export * from "./issue_comment_reaction";
export * from "./issue_sub_issues";
export type TLoader = "init-loader" | "mutation" | undefined;
export type TGroupedIssues = {
[group_id: string]: string[];
};
export type TSubGroupedIssues = {
[sub_grouped_id: string]: {
[group_id: string]: string[];
};
};
export type TUnGroupedIssues = string[];

Some files were not shown because too many files have changed in this diff Show More