Compare commits

...

124 Commits

Author SHA1 Message Date
sriram veeraghanta b72d18079f fix: adding start and target date in issue exporter 2024-11-21 19:24:39 +05:30
sriram veeraghanta a42c69f619 chore: pyporject toml changes 2024-11-21 18:00:02 +05:30
sriram veeraghanta 0dbd4cfe97 chore: formatting changes 2024-11-21 17:42:44 +05:30
Anmol Singh Bhatia a446bc043e [WEB-2765] fix: issue detail page unnecessary scroll (#6068)
* fix: issue dertail page unnecessary scroll

* fix: issue detail sidebar ui
2024-11-21 15:16:47 +05:30
sriram veeraghanta daed58be0f fix: adding new restricted workspace slugs 2024-11-20 20:36:53 +05:30
pablohashescobar ca91d5909b chore: formatting errors 2024-11-20 13:00:13 +05:30
pablohashescobar 3bea2e8d1b chore: fix instance apis 2024-11-20 12:35:13 +05:30
sriram veeraghanta 1325064676 fix: typo and naming conventions 2024-11-20 00:32:30 +05:30
sriram veeraghanta a01a371767 fix: typo fixes 2024-11-20 00:00:04 +05:30
sriram veeraghanta 2d60337eac fix: celery timestamp changes 2024-11-19 20:17:53 +05:30
pablohashescobar f3ac26e5c9 chore: instances 2024-11-19 19:47:13 +05:30
Aaryan Khandelwal d5a55de17a fix: cover image update fix for project and user profile (#6075)
* fix: cover image update payload

* fix: cover image assets

* chore: add gif support

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2024-11-19 18:28:53 +05:30
Prateek Shourya 6f497b024b [WEB-2770] fix: inbox issue detail loader on focus change (#6074) 2024-11-19 17:07:32 +05:30
Nikhil a3e8ee6045 fix: remove caching for user based apis to handle avatar uploads (#6072) 2024-11-19 15:42:10 +05:30
sriram veeraghanta c1ac6e4244 chore: removing dependabot updates alerts 2024-11-18 12:06:13 +05:30
dependabot[bot] 6d98619082 chore(deps): bump actions/checkout from 3 to 4 (#6005)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-16 19:42:08 +05:30
dependabot[bot] 52d3169542 chore(deps): bump softprops/action-gh-release from 2.0.8 to 2.1.0 (#6010)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.0.8 to 2.1.0.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v2.0.8...v2.1.0)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-16 19:40:41 +05:30
dependabot[bot] 5989b1a134 chore(deps): bump github/codeql-action from 2 to 3 (#6011)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-16 19:39:31 +05:30
sriram veeraghanta 291bb5c899 Merge branch 'preview' of github.com:makeplane/plane into preview 2024-11-16 19:37:22 +05:30
sriram veeraghanta 2ef00efaab fix: tubro repo upgrade 2024-11-16 19:37:06 +05:30
dependabot[bot] c5f96466e9 chore(deps): bump cross-spawn in the npm_and_yarn group (#6038)
Bumps the npm_and_yarn group with 1 update: [cross-spawn](https://github.com/moxystudio/node-cross-spawn).


Updates `cross-spawn` from 7.0.3 to 7.0.5
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.5)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-16 18:40:01 +05:30
sriram veeraghanta 35938b57af fix: dependabot security patch only 2024-11-16 18:36:47 +05:30
dependabot[bot] 1b1b160c04 chore(deps): bump docker/build-push-action from 5.1.0 to 6.9.0 (#6004)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5.1.0 to 6.9.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5.1.0...v6.9.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-16 18:30:44 +05:30
sriram veeraghanta 4149e84e62 Create dependabot.yml (#6002) 2024-11-16 18:25:29 +05:30
Aaryan Khandelwal 9408e92e44 Revert "[WEB-1435] dev: conflict free issue descriptions (#5912)" (#6000)
This reverts commit e9680cab74.
2024-11-15 17:13:31 +05:30
Aaryan Khandelwal e9680cab74 [WEB-1435] dev: conflict free issue descriptions (#5912)
* chore: new description binary endpoints

* chore: conflict free issue description

* chore: fix submitting status

* chore: update yjs utils

* chore: handle component re-mounting

* chore: update buffer response type

* chore: add try catch for issue description update

* chore: update buffer response type

* chore: description binary in retrieve

* chore: update issue description hook

* chore: decode description binary

* chore: migrations fixes and cleanup

* chore: migration fixes

* fix: inbox issue description

* chore: move update operations to the issue store

* fix: merge conflicts

* chore: reverted the commit

* chore: removed the unwanted imports

* chore: remove unnecessary props

* chore: remove unused services

* chore: update live server error handling

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-11-15 16:38:58 +05:30
sriram veeraghanta 229610513a fix: django instrumentation fixes 2024-11-13 21:04:16 +05:30
sriram veeraghanta f9d9c92c83 fix: opentelemetry sdk package update 2024-11-13 20:27:47 +05:30
Aaryan Khandelwal 89588d4451 fix: issue and module link validation (#5994)
* fix: issue and module link validation

* chore: removed reset logic
2024-11-13 19:47:30 +05:30
Akshita Goyal 3eb911837c fix: display property in take (#5993) 2024-11-13 18:02:24 +05:30
rahulramesha 4b50b27a74 [WEB-2442] feat: Minor Timeline view Enhancements (#5987)
* fix timeline scroll to the right in some cases

(cherry picked from commit 17043a6c7f)

* add get position based on Date

(cherry picked from commit 2fbe22d689)

* Add sticky block name to enable it to be read throughout the block regardless of scroll position

(cherry picked from commit 447af2e05a)

* Enable blocks to have a single date on the block charts

(cherry picked from commit cb055d566b)

* revert back date-range changes

* change gradient of half blocks on Timeline

* Add instance Id for Timeline Sidebar dragging to avoid enabling dropping of other drag instances

* fix timeline scrolling height
2024-11-13 15:40:37 +05:30
rahulramesha f44db89f41 [WEB-2628] fix: Sorting by estimates (#5988)
* fix estimates sorting in Front end side

* change estimate sorting keys

* - Fix estimate sorting when local db is enabled
- Fix a bug with with sorting on special fields on spreadsheet layout
- Cleanup logging

* Add logic for order by based on layout for special cases of no load

---------

Co-authored-by: Satish Gandham <satish.iitg@gmail.com>
2024-11-13 15:38:43 +05:30
Akshita Goyal 8c3189e1be fix: intake status count (#5990) 2024-11-13 15:38:03 +05:30
sriram veeraghanta eee2145734 fix: code spliting and instance maintenance screens 2024-11-12 19:48:31 +05:30
Aaryan Khandelwal 106710f3d0 fix: custom background color for table header (#5989) 2024-11-12 15:26:57 +05:30
Anmol Singh Bhatia db8c4f92e8 chore: theme and code refactor (#5983)
* chore: added pi colors

* chore: de-dupe modal height

---------

Co-authored-by: gakshita <akshitagoyal1516@gmail.com>
2024-11-11 19:53:43 +05:30
Anmol Singh Bhatia a6cc2c93f8 chore: worklog enhancements (#5982) 2024-11-11 19:27:07 +05:30
Bavisetti Narayan 0428ea06f6 chore: filter the deleted issue assignee (#5984) 2024-11-11 19:25:38 +05:30
Aaryan Khandelwal 7082f7014d style: remove unnecessary bottom padding from the rich text editor (#5976) 2024-11-11 16:11:34 +05:30
Anmol Singh Bhatia c7c729d81b [WEB-2283] fix: create issue modal parent select ui (#5980)
* fix: create issue modal parent select ui

* chore: code refactor
2024-11-11 16:11:10 +05:30
Aaryan Khandelwal 97eb8d43d4 style: updated margins and font styles for editor (#5978)
* style: updated margins and font styles for editor

* fix: code block font size in small font

* fix: remove duplicate code
2024-11-11 16:10:47 +05:30
Anmol Singh Bhatia 1217af1d5f chore: restrict sub-issue to have different project id than parent (#5981) 2024-11-11 16:10:27 +05:30
Bavisetti Narayan 13083a77eb chore: enable intake from project settings (#5977) 2024-11-09 17:01:21 +05:30
Akshita Goyal 0cd36b854e fix: intake loading (#5966)
* fix: intake loading

* fix: image upload in space
2024-11-08 17:17:15 +05:30
Bavisetti Narayan 1d314dd25f fix: renamed inbox to intake (#5967)
* feat: intake

* chore: intake model migration changes

* dev: update dummy data

* dev: add duplicate apis for inbox

* dev: fix external apis

* fix: external apis

* chore: migration file changes

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2024-11-08 17:10:24 +05:30
rahulramesha 1743717351 fix related to activity (#5972) 2024-11-08 17:09:49 +05:30
Satish Gandham acba451803 [WEB-2706] fix: Add fallback when db initialisation fails (#5973)
* Add fallback when db initialization fails

* add checks for instance.exec

* chore: convert issue boolean fields to actual boolean value.

* change instance exec code

* sync issue to local db when inbox issue is accepted and draft issue is moved to project

* chore: added project and workspace keys

---------

Co-authored-by: rahulramesha <rahulramesham@gmail.com>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-11-08 17:09:26 +05:30
Aaryan Khandelwal 2193e8c79c fix: editor user config (#5974) 2024-11-08 13:30:06 +05:30
Anmol Singh Bhatia 4c6ab984c3 [WEB-2742] chore: issue link ui revamp (#5971)
* chore-issue-link-ui

* chore: issue link ui revamp
2024-11-07 19:24:15 +05:30
Prateek Shourya 7574206a41 [WEB-2554] improvement: dashboard sidebar list items. (#5970) 2024-11-07 15:31:28 +05:30
Anmol Singh Bhatia eebc327b10 chore: app sidebar behaviour (#5964) 2024-11-06 18:36:23 +05:30
Prateek Shourya e19cb012be [WEB-2728] improvement: add true-transparent variant for textarea. (#5960) 2024-11-06 16:56:15 +05:30
guru_sainath 9d1253a61d chore: infra update for maintenance mode (#5963) 2024-11-06 15:13:51 +05:30
Bavisetti Narayan 56755b0e9c chore: intake migration (#5950)
* chore: intake migration

* chore: removed the enum

* chore: removed the source type enum

* chore: changed the migration file
2024-11-05 19:21:20 +05:30
rahulramesha 438d1bcfbd add missing config to get issues api call (#5955) 2024-11-05 17:50:23 +05:30
Akshita Goyal 45a5cf5119 fix: editor height (#5953)
* fix: editor height

* fix: removed unwanted class

* fix: editor height
2024-11-05 17:47:39 +05:30
Aaryan Khandelwal b4de055463 [PULSE-42] feat: text alignment for all editors (#5847)
* feat: text alignment for editors

* fix: text alignment types

* fix: build errors

* fix: build error

* fix: toolbar movement post alignment selection

* fix: callout type

* fix: image node types

* chore: add ts error warning
2024-11-05 17:46:34 +05:30
Aaryan Khandelwal bb311b750f fix: wrong token being passed in the read-only editor (#5954)
* fix: wrong token

* chore: update useMemo dependencies
2024-11-05 17:45:53 +05:30
Anmol Singh Bhatia ea8583b2d4 chore: code refactor (#5952)
* chore: code refactor

* chore: code refactor
2024-11-05 17:04:03 +05:30
Akshita Goyal eed2ca77ef fix: added workspaceslug in renderChildren of project settings (#5951)
* fix: added workspaceslug in renderChildren of project settings

* fix: updated apis

* fix: types

* fix: added editor

* fix: handled avatar for intake
2024-11-05 16:07:27 +05:30
Akshita Goyal 9309d1b574 feat: Pi chat (#5933)
* fix: added pi chat

* fix: added bot

* fix: removed pi chat from community version

* fix: removed unwanted files

* fix: removed unused import
2024-11-05 15:16:58 +05:30
Aaryan Khandelwal f205d72782 fix: floating toolbar max width (#5949) 2024-11-04 20:17:20 +05:30
rahulramesha 3d2fe7841f fix issues fetching while changing filters by making sure to pass the abort controller config to apis (#5948) 2024-11-04 20:16:56 +05:30
rahulramesha 71589f93ca [WEB-2442] fix : Timeline layout bugs (#5946)
* fix relation creation and removal for Issue relations

* fix Scrolling to block when the block is beyond current chart's limits

* fix dark mode for timeline layout

* use a hook to get the current relations available in the environment, instead of directly importing it

* Update relation activity for all the relations
2024-11-04 16:55:38 +05:30
Satish Gandham a1bfde6af9 [WEB-2706] fix: Fix issue with SQLite transactions (#5934)
* - Fix transaction within transaction issue
- Close DB handles on reload
- Fix GET_ISSUES tracking

* Cleanup stray code

* Fix lint error

* Possible fix for NoModificationAllowedError
2024-11-04 16:54:13 +05:30
Lakhan Baheti 20b2a70939 fix: global css conflict (#5945) 2024-11-04 16:15:17 +05:30
Prateek Shourya 914811b643 fix: build error for product updates modal. (#5944) 2024-11-04 14:04:59 +05:30
Nikhil 0dead39fd1 chore: device migration (#5939)
* chore: device migration

* chore: devices

* chore: update device migrations

* chore: update migration

* chore: update migrations

* chore: update device migrations
2024-11-01 22:40:39 +05:30
sriram veeraghanta 27d7d91185 fix: new set of migrations in db models 2024-11-01 21:24:57 +05:30
Lakhan Baheti 3696062372 [WEB-2730] chore: core/editor updates to support mobile editor (#5910)
* added editor changes w.r.t mobile-editor

* added external extensions option

* fix: type errors in image block

* added on transaction method

* fix: optional prop fixed

* fix: memoize the extensions array

* fix: added missing deps

* fix: image component types

* fix: remove range prop

* fix: type fixes and better names of img src

* fix: image load blinking

* fix: code review

* fix: props code review

* fix: coderabbit review

---------

Co-authored-by: Palanikannan M <akashmalinimurugu@gmail.com>
2024-10-30 17:39:02 +05:30
Lakhan Baheti 8ea34b5995 [WEB-2729] chore: updated live server auth cookies handling (#5913)
* chore: updated live server auth cookies handling

* chore: update token parsing logic

* fix: types and better logical seperation between the existing two tokens

* fix: better fallback to use request headers for cookies

---------

Co-authored-by: Palanikannan M <akashmalinimurugu@gmail.com>
2024-10-30 17:38:29 +05:30
Bavisetti Narayan 403482fa6e fix: workspace user property migration (#5908)
* fix: workspace user property migration

* fix: issue relations migration
2024-10-30 13:52:14 +05:30
Nikhil fe18eae8cd fix: integrity error on account creation (#5876)
* fix: integrity error on account creation

* fix: exception handling
2024-10-30 13:46:05 +05:30
rahulramesha 3f429a1dab minor build fix (#5929) 2024-10-29 20:51:56 +05:30
Ketan Sharma 22b616b03c [WEB-2449] fix: admin is not able to edit issues in notifications peek overview (#5877)
* fix backend

* fix missing arguments for allow permissions

* Revert "fix backend"

This reverts commit 208636d7c8.
2024-10-29 19:46:20 +05:30
Anmol Singh Bhatia 57eb08c8a2 chore: code refactoring (#5928)
* chore: de dupe code splitting

* chore: code refactor
2024-10-29 19:39:55 +05:30
Prateek Shourya 4bc751b7ab [WEB-2500] feat: Product updates modal (What's new in Plane) (#5690)
* [WEB-2500] feat: Product updates modal (What's new in Plane)

* fix: build errors.

* fix: lint errors resolved.

* chore: minor improvements.

* chore: minor fixes
2024-10-29 19:26:00 +05:30
Aaryan Khandelwal c423d7d9df [WEB-2717] chore: implemented issue attachment upload progress (#5901)
* chore: added attachment upload progress

* chore: add debounce while updating the upload status

* chore: update percentage calc logic

* chore: update debounce interval
2024-10-29 19:22:29 +05:30
rahulramesha 538e78f135 refactor timeline store for code splitting (#5926) 2024-10-29 17:57:45 +05:30
Aaryan Khandelwal b4bbe3a8ba fix: change html tag name for callout (#5924) 2024-10-29 14:12:12 +05:30
Prateek Shourya b67f352b90 fix: lint and build errors (#5923)
* fix: lint errors.

* fix: build errors
2024-10-29 13:45:18 +05:30
Anmol Singh Bhatia 8829575780 chore: app sidebar add issue button improvement (#5921) 2024-10-29 13:42:42 +05:30
rahulramesha 724adeff5c [WEB-2442] fix: Timeline Improvements and bug fixes (#5922)
* improve auto scroller logic

* fix drag indicator visibility on for blocks

* modify timeline store logic and improve timeline scrolling logic

* fix width of block while dragging with left handle

* fix block arrow direction while block is out of viewport
2024-10-29 13:42:14 +05:30
rahulramesha a88a39fb1e [WEB-2442] feat: Revamp Timeline Layout (#5915)
* chore: added issue relations in issue listing

* chore: added pagination for issue detail endpoint

* chore: bulk date update endpoint

* chore: appended the target date

* chore: issue relation new types defined

* fix: order by and issue filters

* fix: passed order by in pagination

* chore: changed the key for issue dates

* Revamp Timeline Layout

* fix block dragging

* minor ui fixes

* improve auto scroll UX

* remove unused import

* fix timeline layout heights

* modify base timeline store

* Segregate issue relation types

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-10-28 18:03:31 +05:30
Aaryan Khandelwal f986bd83fd fix: callout content not being saved in description html (#5920) 2024-10-28 17:00:32 +05:30
Satish Gandham 6113aefde0 Fix issue with SQLite transactions (#5919) 2024-10-28 14:14:57 +05:30
Bavisetti Narayan 6d08cf2757 fix: rendered the analytics for labels (#5906)
* fix: rendered the analytics for labels

* fix: analytics exports
2024-10-24 20:35:27 +05:30
Bavisetti Narayan 2caf23fb71 fix: background task metadata (#5909) 2024-10-24 20:35:05 +05:30
Bavisetti Narayan b33328dec5 fix: issue retrieval endpoint (#5907) 2024-10-24 20:33:16 +05:30
Aaryan Khandelwal 14b31e3fcd [PULSE-36] feat: callout component for pages and issue descriptions (#5856)
* feat: editor callouts

* chore: backspace action updated

* chore: update callout attributes types

* chore: revert emoji picker changes

* chore: removed class atrribute

* chore: added sanitization for local storage values

* chore: disable emoji picker search
2024-10-24 15:36:38 +05:30
Satish Gandham 9fb353ef54 [WEB-2706] chore: Switch to wa-sqlite (#5859)
* fix layout switching when filter is not yet completely fetched

* add layout in issue filter params

* Handle cases when DB intilization failed

* chore: permission layer and updated issues v1 query from workspace to project level

* - Switch to using wa-sqlite instead of sqlite-wasm

* Code cleanup and fix indexes

* Add missing files

* - Import only required functions from sentry
- Wait till all the tables are created

* Skip workspace sync if one is already in progress.

* Sync workspace without using transaction

* Minor cleanup

* Close DB connection before deleting files
Fix clear OPFS on safari

* Fix type issue

* Improve issue insert performance

* Refactor workspace sync

* Close the DB connection while switching workspaces

* Update web/core/local-db/worker/db.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Worker cleanup and error handling

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update web/core/local-db/worker/db.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update web/core/local-db/storage.sqlite.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update web/core/local-db/worker/db.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Code cleanup

* Set default order by to created at and descending

* Wait for transactions to complete.

---------

Co-authored-by: rahulramesha <rahulramesham@gmail.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2024-10-24 15:35:02 +05:30
Ketan Sharma ad25a972a1 [WEB-2587] fix: hide log work button for guest user (#5787)
* fix the rendering logic

* fix handle nullish value
2024-10-24 14:48:59 +05:30
Ketan Sharma 4157f3750b add missing background color (#5789) 2024-10-24 14:46:56 +05:30
Ketan Sharma d7c5645948 [WEB-2606] fix: project members shouldn't be able to change others roles (#5802)
* [WEB-2606] fix: project members should not be able to change other project member's roles

* add better logic
2024-10-24 14:46:10 +05:30
Anmol Singh Bhatia 8d837eddb3 chore: calendar current date indicator improvement (#5880) 2024-10-24 14:42:44 +05:30
Anmol Singh Bhatia 0312455d66 fix: project state setting dnd (#5881) 2024-10-24 14:41:35 +05:30
Prateek Shourya e4e83a947a [WEB-2479] fix: merge default and archived issue details endpoint. (#5882) 2024-10-24 14:40:50 +05:30
Akshita Goyal 2ecc379486 fix: truncated project name in analytics dropdown (#5883) 2024-10-24 14:39:32 +05:30
Prateek Shourya bf220666dd [WEB-2326] fix: issue activity mutation on attachments upload. (#5886) 2024-10-24 14:36:30 +05:30
Anmol Singh Bhatia 074ad6d1a4 chore: intake issue back date snooze disabled (#5888) 2024-10-24 14:35:57 +05:30
Bavisetti Narayan 4b815f3769 fix: issue attachment uploads (#5904) 2024-10-23 21:04:10 +05:30
Anmol Singh Bhatia 56bb6e1f48 fix: draft issue type update outside click (#5902) 2024-10-23 20:11:28 +05:30
Bavisetti Narayan 5afa686a21 chore: issue attachment deletion (#5903) 2024-10-23 20:11:01 +05:30
Anmol Singh Bhatia 25a410719b fix: intake issue description and navigation (#5900) 2024-10-23 16:46:28 +05:30
Anmol Singh Bhatia cbfcbba5d1 [WEB-2709] chore: intake issue navigation improvement (#5891)
* chore: intake issue navigation improvement

* chore: code refactor

* chore: intake issue navigation improvement

* chore: intake issue navigation improvement
2024-10-23 15:19:43 +05:30
Anmol Singh Bhatia c4421f5f97 fix: issue widget modal rendering (#5896) 2024-10-23 15:19:26 +05:30
Anmol Singh Bhatia 84c06c4713 fix: guest user intake issue edit validation (#5898) 2024-10-23 15:19:10 +05:30
Bavisetti Narayan 6df98099f5 chore: filter the deleted issues stats (#5893) 2024-10-22 20:51:11 +05:30
Bavisetti Narayan 295f094916 chore: changed the annotate for cycle id (#5892) 2024-10-22 19:02:05 +05:30
Akshita Goyal d859ab9c39 [WEB-2708] fix: intake module and cycle addition fixed (#5890)
* fix: intake module and cycle addition fixed

* chore: fixed the search endpoint

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-10-22 17:59:07 +05:30
Anmol Singh Bhatia 36b868e375 [WEB-2707] fix: draft issue module update and code refactor (#5889)
* chore: draft issue module update

* chore: code refactor
2024-10-22 16:16:29 +05:30
Aaryan Khandelwal 4c20be6cf2 [PE-68] fix: markdown transformation of mention and custom image components (#5864)
* fix: markdown content for mention and custom image extensions

* style: update issue embed upgrade card

* chore: added string escapes
2024-10-22 14:29:50 +05:30
Bavisetti Narayan 7bf4620bc1 chore: soft deletion of cycle and module (#5884)
* chore: soft deletion of cycle and module

* chore: cycle module soft delete

* chore: added the deletion task

* chore: updated the env example

* chore: cycle issue unique constraints

* chore: udpated the Q operator
2024-10-22 14:21:26 +05:30
Nikhil 00eff43f4d fix: bucket policy script to handle error conditions (#5887)
* fix: bucket policy script to handle error conditions

* dev: handle edge cases
2024-10-22 14:19:43 +05:30
sriram veeraghanta 3d3f1b8f74 fix: typescript version consistency 2024-10-22 14:13:28 +05:30
sriram veeraghanta b87516b0be chore: fixing inconsistent dependencies across the platform (#5885)
* chore: fixing inconsistent dependies across the platform

* fix: fixing peer dependencies

* chore: yarn lock regeneration
2024-10-22 14:03:34 +05:30
Anmol Singh Bhatia 8a1d3c4cf9 chore: urgent priority icon improvement (#5879) 2024-10-22 13:25:22 +05:30
Akshita Goyal 0f25f39404 WEB-2381 Chore: intake refactor (#5752)
* chore: intake emails and forms

* fix: moved files to ee

* fix: intake form ui

* fix: settings apis integrated

* fix: removed publish api

* fix: removed space app

* fix: lint issue

* fix: removed logs

* fix: removed comment

* fix: improved success image
2024-10-22 12:09:03 +05:30
sriram veeraghanta fb49644185 fix: renaming the action and formatting 2024-10-21 19:26:16 +05:30
Nikhil b745a29454 fix: credential sending for file uploads (#5869) 2024-10-21 17:46:46 +05:30
M. Palanikannan c940a2921e fix: validation of public and private assets (#5878) 2024-10-21 15:59:44 +05:30
Anmol Singh Bhatia 6f8df3279c [WEB-2681] fix: module progress indicator (#5842)
* fix: module progress indicator

* fix: module progress indicator
2024-10-21 15:48:35 +05:30
Prateek Shourya b833e3b10c [WEB-2674] chore: open parent issues in peek-overview from the parent badge. (#5872)
* [WEB-2674] chore: open parent issues in peek-overview from the parent badge.

* chore: remove `_blank` target from ControlLink.
2024-10-21 14:20:00 +05:30
M. Palanikannan 5a0dc4a65a [PE-69] fix: image restoration fixed for new images in private bucket (#5839)
* regression: image aspect ratio fix

* fix: name of variables changed for clarity

* fix: restore only on error

* fix: restore image by handling it inside the image component

* fix: image restoration fixed and aspect ratio added to old images to stop updates on load

* fix: added back restoring logic for public images

* fix: add conditions

* fix: image attributes types

* fix: return for old images

* fix: remove passive false

* fix: eslint fixes

* fix: stopping infinite loading scenarios while restoring from error
2024-10-21 14:17:05 +05:30
684 changed files with 17966 additions and 14831 deletions
+2 -2
View File
@@ -83,7 +83,7 @@ jobs:
endpoint: ${{ env.BUILDX_ENDPOINT }}
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v5.1.0
uses: docker/build-push-action@v6.9.0
with:
context: ./aio
file: ./aio/Dockerfile-base-full
@@ -124,7 +124,7 @@ jobs:
endpoint: ${{ env.BUILDX_ENDPOINT }}
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v5.1.0
uses: docker/build-push-action@v6.9.0
with:
context: ./aio
file: ./aio/Dockerfile-base-slim
+2 -2
View File
@@ -128,7 +128,7 @@ jobs:
uses: actions/checkout@v4
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v5.1.0
uses: docker/build-push-action@v6.9.0
with:
context: .
file: ./aio/Dockerfile-app
@@ -188,7 +188,7 @@ jobs:
uses: actions/checkout@v4
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v5.1.0
uses: docker/build-push-action@v6.9.0
with:
context: .
file: ./aio/Dockerfile-app
+11 -11
View File
@@ -170,7 +170,7 @@ jobs:
name: Checkout Files
uses: actions/checkout@v4
- name: Admin Build and Push
uses: ./.github/actions/buildpush-action
uses: ./.github/actions/build-push-ce
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
@@ -196,7 +196,7 @@ jobs:
name: Checkout Files
uses: actions/checkout@v4
- name: Web Build and Push
uses: ./.github/actions/buildpush-action
uses: ./.github/actions/build-push-ce
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
@@ -222,7 +222,7 @@ jobs:
name: Checkout Files
uses: actions/checkout@v4
- name: Space Build and Push
uses: ./.github/actions/buildpush-action
uses: ./.github/actions/build-push-ce
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
@@ -248,7 +248,7 @@ jobs:
name: Checkout Files
uses: actions/checkout@v4
- name: Live Build and Push
uses: ./.github/actions/buildpush-action
uses: ./.github/actions/build-push-ce
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
@@ -274,7 +274,7 @@ jobs:
name: Checkout Files
uses: actions/checkout@v4
- name: Backend Build and Push
uses: ./.github/actions/buildpush-action
uses: ./.github/actions/build-push-ce
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
@@ -300,7 +300,7 @@ jobs:
name: Checkout Files
uses: actions/checkout@v4
- name: Proxy Build and Push
uses: ./.github/actions/buildpush-action
uses: ./.github/actions/build-push-ce
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
@@ -320,7 +320,7 @@ jobs:
if: ${{ needs.branch_build_setup.outputs.build_type == 'Build' }}
name: Attach Assets to Build
runs-on: ubuntu-20.04
needs: [ branch_build_setup ]
needs: [branch_build_setup]
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -340,7 +340,7 @@ jobs:
${{ github.workspace }}/deploy/selfhost/restore.sh
${{ github.workspace }}/deploy/selfhost/docker-compose.yml
${{ github.workspace }}/deploy/selfhost/variables.env
publish_release:
if: ${{ needs.branch_build_setup.outputs.build_type == 'Release' }}
name: Build Release
@@ -353,7 +353,7 @@ jobs:
branch_build_push_space,
branch_build_push_live,
branch_build_push_apiserver,
branch_build_push_proxy
branch_build_push_proxy,
]
env:
REL_VERSION: ${{ needs.branch_build_setup.outputs.release_version }}
@@ -367,7 +367,7 @@ jobs:
- name: Create Release
id: create_release
uses: softprops/action-gh-release@v2.0.8
uses: softprops/action-gh-release@v2.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
with:
@@ -380,4 +380,4 @@ jobs:
${{ github.workspace }}/deploy/selfhost/setup.sh
${{ github.workspace }}/deploy/selfhost/restore.sh
${{ github.workspace }}/deploy/selfhost/docker-compose.yml
${{ github.workspace }}/deploy/selfhost/variables.env
${{ github.workspace }}/deploy/selfhost/variables.env
+4 -4
View File
@@ -29,11 +29,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -46,7 +46,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
uses: github/codeql-action/autobuild@v3
# ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -59,6 +59,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
+1 -1
View File
@@ -79,7 +79,7 @@ jobs:
uses: actions/checkout@v4
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v5.1.0
uses: docker/build-push-action@v6.9.0
with:
context: .
file: ./aio/Dockerfile-app
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for all branches and tags
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
contents: read
steps:
- name: Checkout Code
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4
with:
persist-credentials: false
fetch-depth: 0
+1 -1
View File
@@ -41,7 +41,7 @@
"@plane/eslint-config": "*",
"@plane/typescript-config": "*",
"@types/node": "18.16.1",
"@types/react": "^18.2.48",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.2.18",
"@types/uuid": "^9.0.8",
"@types/zxcvbn": "^4.4.4",
+2 -1
View File
@@ -57,5 +57,6 @@ ADMIN_BASE_URL=
SPACE_BASE_URL=
APP_BASE_URL=
# Hard delete files after days
HARD_DELETE_AFTER_DAYS=60
HARD_DELETE_AFTER_DAYS=60
+1
View File
@@ -4,6 +4,7 @@ FROM python:3.12.5-alpine AS backend
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
ENV INSTANCE_CHANGELOG_URL https://api.plane.so/api/public/anchor/8e1c2e4c7bc5493eb7731be3862f6960/pages/
WORKDIR /code
+1
View File
@@ -4,6 +4,7 @@ FROM python:3.12.5-alpine AS backend
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
ENV INSTANCE_CHANGELOG_URL https://api.plane.so/api/public/anchor/8e1c2e4c7bc5493eb7731be3862f6960/pages/
RUN apk --no-cache add \
"bash~=5.2" \
+7 -21
View File
@@ -26,9 +26,7 @@ def update_description():
updated_issues.append(issue)
Issue.objects.bulk_update(
updated_issues,
["description_html", "description_stripped"],
batch_size=100,
updated_issues, ["description_html", "description_stripped"], batch_size=100
)
print("Success")
except Exception as e:
@@ -42,9 +40,7 @@ def update_comments():
updated_issue_comments = []
for issue_comment in issue_comments:
issue_comment.comment_html = (
f"<p>{issue_comment.comment_stripped}</p>"
)
issue_comment.comment_html = f"<p>{issue_comment.comment_stripped}</p>"
updated_issue_comments.append(issue_comment)
IssueComment.objects.bulk_update(
@@ -103,9 +99,7 @@ def updated_issue_sort_order():
issue.sort_order = issue.sequence_id * random.randint(100, 500)
updated_issues.append(issue)
Issue.objects.bulk_update(
updated_issues, ["sort_order"], batch_size=100
)
Issue.objects.bulk_update(updated_issues, ["sort_order"], batch_size=100)
print("Success")
except Exception as e:
print(e)
@@ -143,9 +137,7 @@ def update_project_cover_images():
project.cover_image = project_cover_images[random.randint(0, 19)]
updated_projects.append(project)
Project.objects.bulk_update(
updated_projects, ["cover_image"], batch_size=100
)
Project.objects.bulk_update(updated_projects, ["cover_image"], batch_size=100)
print("Success")
except Exception as e:
print(e)
@@ -194,9 +186,7 @@ def update_label_color():
def create_slack_integration():
try:
_ = Integration.objects.create(
provider="slack", network=2, title="Slack"
)
_ = Integration.objects.create(provider="slack", network=2, title="Slack")
print("Success")
except Exception as e:
print(e)
@@ -222,16 +212,12 @@ def update_integration_verified():
def update_start_date():
try:
issues = Issue.objects.filter(
state__group__in=["started", "completed"]
)
issues = Issue.objects.filter(state__group__in=["started", "completed"])
updated_issues = []
for issue in issues:
issue.start_date = issue.created_at.date()
updated_issues.append(issue)
Issue.objects.bulk_update(
updated_issues, ["start_date"], batch_size=500
)
Issue.objects.bulk_update(updated_issues, ["start_date"], batch_size=500)
print("Success")
except Exception as e:
print(e)
+1 -3
View File
@@ -3,9 +3,7 @@ import os
import sys
if __name__ == "__main__":
os.environ.setdefault(
"DJANGO_SETTINGS_MODULE", "plane.settings.production"
)
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
@@ -25,10 +25,7 @@ class APIKeyAuthentication(authentication.BaseAuthentication):
def validate_api_token(self, token):
try:
api_token = APIToken.objects.get(
Q(
Q(expired_at__gt=timezone.now())
| Q(expired_at__isnull=True)
),
Q(Q(expired_at__gt=timezone.now()) | Q(expired_at__isnull=True)),
token=token,
is_active=True,
)
+1 -1
View File
@@ -80,4 +80,4 @@ class ServiceTokenRateThrottle(SimpleRateThrottle):
request.META["X-RateLimit-Remaining"] = max(0, available)
request.META["X-RateLimit-Reset"] = reset_time
return allowed
return allowed
+2 -6
View File
@@ -13,9 +13,5 @@ from .issue import (
)
from .state import StateLiteSerializer, StateSerializer
from .cycle import CycleSerializer, CycleIssueSerializer, CycleLiteSerializer
from .module import (
ModuleSerializer,
ModuleIssueSerializer,
ModuleLiteSerializer,
)
from .inbox import InboxIssueSerializer
from .module import ModuleSerializer, ModuleIssueSerializer, ModuleLiteSerializer
from .intake import IntakeIssueSerializer
+1 -3
View File
@@ -102,8 +102,6 @@ class BaseSerializer(serializers.ModelSerializer):
response[expand] = exp_serializer.data
else:
# You might need to handle this case differently
response[expand] = getattr(
instance, f"{expand}_id", None
)
response[expand] = getattr(instance, f"{expand}_id", None)
return response
+2 -8
View File
@@ -23,9 +23,7 @@ class CycleSerializer(BaseSerializer):
and data.get("end_date", None) is not None
and data.get("start_date", None) > data.get("end_date", None)
):
raise serializers.ValidationError(
"Start date cannot exceed end date"
)
raise serializers.ValidationError("Start date cannot exceed end date")
return data
class Meta:
@@ -50,11 +48,7 @@ class CycleIssueSerializer(BaseSerializer):
class Meta:
model = CycleIssue
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"cycle",
]
read_only_fields = ["workspace", "project", "cycle"]
class CycleLiteSerializer(BaseSerializer):
@@ -1,15 +1,16 @@
# Module improts
from .base import BaseSerializer
from .issue import IssueExpandSerializer
from plane.db.models import InboxIssue
from plane.db.models import IntakeIssue
from rest_framework import serializers
class InboxIssueSerializer(BaseSerializer):
class IntakeIssueSerializer(BaseSerializer):
issue_detail = IssueExpandSerializer(read_only=True, source="issue")
inbox = serializers.UUIDField(source="intake.id", read_only=True)
class Meta:
model = InboxIssue
model = IntakeIssue
fields = "__all__"
read_only_fields = [
"id",
+20 -63
View File
@@ -49,25 +49,13 @@ class IssueSerializer(BaseSerializer):
required=False,
)
type_id = serializers.PrimaryKeyRelatedField(
source="type",
queryset=IssueType.objects.all(),
required=False,
allow_null=True,
source="type", queryset=IssueType.objects.all(), required=False, allow_null=True
)
class Meta:
model = Issue
read_only_fields = [
"id",
"workspace",
"project",
"updated_by",
"updated_at",
]
exclude = [
"description",
"description_stripped",
]
read_only_fields = ["id", "workspace", "project", "updated_by", "updated_at"]
exclude = ["description", "description_stripped"]
def validate(self, data):
if (
@@ -75,9 +63,7 @@ class IssueSerializer(BaseSerializer):
and data.get("target_date", None) is not None
and data.get("start_date", None) > data.get("target_date", None)
):
raise serializers.ValidationError(
"Start date cannot exceed target date"
)
raise serializers.ValidationError("Start date cannot exceed target date")
try:
if data.get("description_html", None) is not None:
@@ -99,16 +85,14 @@ class IssueSerializer(BaseSerializer):
# Validate labels are from project
if data.get("labels", []):
data["labels"] = Label.objects.filter(
project_id=self.context.get("project_id"),
id__in=data["labels"],
project_id=self.context.get("project_id"), id__in=data["labels"]
).values_list("id", flat=True)
# Check state is from the project only else raise validation error
if (
data.get("state")
and not State.objects.filter(
project_id=self.context.get("project_id"),
pk=data.get("state").id,
project_id=self.context.get("project_id"), pk=data.get("state").id
).exists()
):
raise serializers.ValidationError(
@@ -119,8 +103,7 @@ class IssueSerializer(BaseSerializer):
if (
data.get("parent")
and not Issue.objects.filter(
workspace_id=self.context.get("workspace_id"),
pk=data.get("parent").id,
workspace_id=self.context.get("workspace_id"), pk=data.get("parent").id
).exists()
):
raise serializers.ValidationError(
@@ -147,9 +130,7 @@ class IssueSerializer(BaseSerializer):
issue_type = issue_type
issue = Issue.objects.create(
**validated_data,
project_id=project_id,
type=issue_type,
**validated_data, project_id=project_id, type=issue_type
)
# Issue Audit Users
@@ -212,7 +193,7 @@ class IssueSerializer(BaseSerializer):
updated_by_id = instance.updated_by_id
if assignees is not None:
IssueAssignee.objects.filter(issue=instance).delete(soft=False)
IssueAssignee.objects.filter(issue=instance).delete()
IssueAssignee.objects.bulk_create(
[
IssueAssignee(
@@ -229,7 +210,7 @@ class IssueSerializer(BaseSerializer):
)
if labels is not None:
IssueLabel.objects.filter(issue=instance).delete(soft=False)
IssueLabel.objects.filter(issue=instance).delete()
IssueLabel.objects.bulk_create(
[
IssueLabel(
@@ -264,13 +245,9 @@ class IssueSerializer(BaseSerializer):
]
if "labels" in self.fields:
if "labels" in self.expand:
data["labels"] = LabelSerializer(
instance.labels.all(), many=True
).data
data["labels"] = LabelSerializer(instance.labels.all(), many=True).data
else:
data["labels"] = [
str(label.id) for label in instance.labels.all()
]
data["labels"] = [str(label.id) for label in instance.labels.all()]
return data
@@ -278,11 +255,7 @@ class IssueSerializer(BaseSerializer):
class IssueLiteSerializer(BaseSerializer):
class Meta:
model = Issue
fields = [
"id",
"sequence_id",
"project_id",
]
fields = ["id", "sequence_id", "project_id"]
read_only_fields = fields
@@ -334,8 +307,7 @@ class IssueLinkSerializer(BaseSerializer):
# Validation if url already exists
def create(self, validated_data):
if IssueLink.objects.filter(
url=validated_data.get("url"),
issue_id=validated_data.get("issue_id"),
url=validated_data.get("url"), issue_id=validated_data.get("issue_id")
).exists():
raise serializers.ValidationError(
{"error": "URL already exists for this Issue"}
@@ -345,8 +317,7 @@ class IssueLinkSerializer(BaseSerializer):
def update(self, instance, validated_data):
if (
IssueLink.objects.filter(
url=validated_data.get("url"),
issue_id=instance.issue_id,
url=validated_data.get("url"), issue_id=instance.issue_id
)
.exclude(pk=instance.id)
.exists()
@@ -387,10 +358,7 @@ class IssueCommentSerializer(BaseSerializer):
"created_at",
"updated_at",
]
exclude = [
"comment_stripped",
"comment_json",
]
exclude = ["comment_stripped", "comment_json"]
def validate(self, data):
try:
@@ -407,38 +375,27 @@ class IssueCommentSerializer(BaseSerializer):
class IssueActivitySerializer(BaseSerializer):
class Meta:
model = IssueActivity
exclude = [
"created_by",
"updated_by",
]
exclude = ["created_by", "updated_by"]
class CycleIssueSerializer(BaseSerializer):
cycle = CycleSerializer(read_only=True)
class Meta:
fields = [
"cycle",
]
fields = ["cycle"]
class ModuleIssueSerializer(BaseSerializer):
module = ModuleSerializer(read_only=True)
class Meta:
fields = [
"module",
]
fields = ["module"]
class LabelLiteSerializer(BaseSerializer):
class Meta:
model = Label
fields = [
"id",
"name",
"color",
]
fields = ["id", "name", "color"]
class IssueExpandSerializer(BaseSerializer):
+5 -13
View File
@@ -53,14 +53,11 @@ class ModuleSerializer(BaseSerializer):
and data.get("target_date", None) is not None
and data.get("start_date", None) > data.get("target_date", None)
):
raise serializers.ValidationError(
"Start date cannot exceed target date"
)
raise serializers.ValidationError("Start date cannot exceed target date")
if data.get("members", []):
data["members"] = ProjectMember.objects.filter(
project_id=self.context.get("project_id"),
member_id__in=data["members"],
project_id=self.context.get("project_id"), member_id__in=data["members"]
).values_list("member_id", flat=True)
return data
@@ -74,9 +71,7 @@ class ModuleSerializer(BaseSerializer):
module_name = validated_data.get("name")
if module_name:
# Lookup for the module name in the module table for that project
if Module.objects.filter(
name=module_name, project_id=project_id
).exists():
if Module.objects.filter(name=module_name, project_id=project_id).exists():
raise serializers.ValidationError(
{"error": "Module with this name already exists"}
)
@@ -107,9 +102,7 @@ class ModuleSerializer(BaseSerializer):
if module_name:
# Lookup for the module name in the module table for that project
if (
Module.objects.filter(
name=module_name, project=instance.project
)
Module.objects.filter(name=module_name, project=instance.project)
.exclude(id=instance.id)
.exists()
):
@@ -172,8 +165,7 @@ class ModuleLinkSerializer(BaseSerializer):
# Validation if url already exists
def create(self, validated_data):
if ModuleLink.objects.filter(
url=validated_data.get("url"),
module_id=validated_data.get("module_id"),
url=validated_data.get("url"), module_id=validated_data.get("module_id")
).exists():
raise serializers.ValidationError(
{"error": "URL already exists for this Issue"}
+4 -11
View File
@@ -2,11 +2,7 @@
from rest_framework import serializers
# Module imports
from plane.db.models import (
Project,
ProjectIdentifier,
WorkspaceMember,
)
from plane.db.models import Project, ProjectIdentifier, WorkspaceMember
from .base import BaseSerializer
@@ -20,6 +16,7 @@ class ProjectSerializer(BaseSerializer):
member_role = serializers.IntegerField(read_only=True)
is_deployed = serializers.BooleanField(read_only=True)
cover_image_url = serializers.CharField(read_only=True)
inbox_view = serializers.BooleanField(read_only=True, source="intake_view")
class Meta:
model = Project
@@ -66,16 +63,12 @@ class ProjectSerializer(BaseSerializer):
def create(self, validated_data):
identifier = validated_data.get("identifier", "").strip().upper()
if identifier == "":
raise serializers.ValidationError(
detail="Project Identifier is required"
)
raise serializers.ValidationError(detail="Project Identifier is required")
if ProjectIdentifier.objects.filter(
name=identifier, workspace_id=self.context["workspace_id"]
).exists():
raise serializers.ValidationError(
detail="Project Identifier is taken"
)
raise serializers.ValidationError(detail="Project Identifier is taken")
project = Project.objects.create(
**validated_data, workspace_id=self.context["workspace_id"]
+4 -9
View File
@@ -7,9 +7,9 @@ class StateSerializer(BaseSerializer):
def validate(self, data):
# If the default is being provided then make all other states default False
if data.get("default", False):
State.objects.filter(
project_id=self.context.get("project_id")
).update(default=False)
State.objects.filter(project_id=self.context.get("project_id")).update(
default=False
)
return data
class Meta:
@@ -30,10 +30,5 @@ class StateSerializer(BaseSerializer):
class StateLiteSerializer(BaseSerializer):
class Meta:
model = State
fields = [
"id",
"name",
"color",
"group",
]
fields = ["id", "name", "color", "group"]
read_only_fields = fields
+1 -5
View File
@@ -8,9 +8,5 @@ class WorkspaceLiteSerializer(BaseSerializer):
class Meta:
model = Workspace
fields = [
"name",
"slug",
"id",
]
fields = ["name", "slug", "id"]
read_only_fields = fields
+2 -2
View File
@@ -3,7 +3,7 @@ from .state import urlpatterns as state_patterns
from .issue import urlpatterns as issue_patterns
from .cycle import urlpatterns as cycle_patterns
from .module import urlpatterns as module_patterns
from .inbox import urlpatterns as inbox_patterns
from .intake import urlpatterns as intake_patterns
from .member import urlpatterns as member_patterns
urlpatterns = [
@@ -12,6 +12,6 @@ urlpatterns = [
*issue_patterns,
*cycle_patterns,
*module_patterns,
*inbox_patterns,
*intake_patterns,
*member_patterns,
]
-17
View File
@@ -1,17 +0,0 @@
from django.urls import path
from plane.api.views import InboxIssueAPIEndpoint
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/",
InboxIssueAPIEndpoint.as_view(),
name="inbox-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:issue_id>/",
InboxIssueAPIEndpoint.as_view(),
name="inbox-issue",
),
]
+27
View File
@@ -0,0 +1,27 @@
from django.urls import path
from plane.api.views import IntakeIssueAPIEndpoint
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/",
IntakeIssueAPIEndpoint.as_view(),
name="inbox-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:issue_id>/",
IntakeIssueAPIEndpoint.as_view(),
name="inbox-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-issues/",
IntakeIssueAPIEndpoint.as_view(),
name="intake-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-issues/<uuid:issue_id>/",
IntakeIssueAPIEndpoint.as_view(),
name="intake-issue",
),
]
+2 -4
View File
@@ -1,13 +1,11 @@
from django.urls import path
from plane.api.views import (
ProjectMemberAPIEndpoint,
)
from plane.api.views import ProjectMemberAPIEndpoint
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<str:project_id>/members/",
ProjectMemberAPIEndpoint.as_view(),
name="users",
),
)
]
+2 -7
View File
@@ -1,15 +1,10 @@
from django.urls import path
from plane.api.views import (
ProjectAPIEndpoint,
ProjectArchiveUnarchiveAPIEndpoint,
)
from plane.api.views import ProjectAPIEndpoint, ProjectArchiveUnarchiveAPIEndpoint
urlpatterns = [
path(
"workspaces/<str:slug>/projects/",
ProjectAPIEndpoint.as_view(),
name="project",
"workspaces/<str:slug>/projects/", ProjectAPIEndpoint.as_view(), name="project"
),
path(
"workspaces/<str:slug>/projects/<uuid:pk>/",
+1 -2
View File
@@ -27,5 +27,4 @@ from .module import (
from .member import ProjectMemberAPIEndpoint
from .inbox import InboxIssueAPIEndpoint
from .intake import IntakeIssueAPIEndpoint
+7 -18
View File
@@ -37,13 +37,9 @@ class TimezoneMixin:
class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
authentication_classes = [
APIKeyAuthentication,
]
authentication_classes = [APIKeyAuthentication]
permission_classes = [
IsAuthenticated,
]
permission_classes = [IsAuthenticated]
def filter_queryset(self, queryset):
for backend in list(self.filter_backends):
@@ -56,8 +52,7 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
if api_key:
service_token = APIToken.objects.filter(
token=api_key,
is_service=True,
token=api_key, is_service=True
).first()
if service_token:
@@ -123,9 +118,7 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
def finalize_response(self, request, response, *args, **kwargs):
# Call super to get the default response
response = super().finalize_response(
request, response, *args, **kwargs
)
response = super().finalize_response(request, response, *args, **kwargs)
# Add custom headers if they exist in the request META
ratelimit_remaining = request.META.get("X-RateLimit-Remaining")
@@ -154,17 +147,13 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
@property
def fields(self):
fields = [
field
for field in self.request.GET.get("fields", "").split(",")
if field
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
expand for expand in self.request.GET.get("expand", "").split(",") if expand
]
return expand if expand else None
return expand if expand else None
+73 -138
View File
@@ -25,10 +25,7 @@ from rest_framework import status
from rest_framework.response import Response
# Module imports
from plane.api.serializers import (
CycleIssueSerializer,
CycleSerializer,
)
from plane.api.serializers import CycleIssueSerializer, CycleSerializer
from plane.app.permissions import ProjectEntityPermission
from plane.bgtasks.issue_activities_task import issue_activity
from plane.db.models import (
@@ -57,9 +54,7 @@ class CycleAPIEndpoint(BaseAPIView):
serializer_class = CycleSerializer
model = Cycle
webhook_event = "cycle"
permission_classes = [
ProjectEntityPermission,
]
permission_classes = [ProjectEntityPermission]
def get_queryset(self):
return (
@@ -78,6 +73,7 @@ class CycleAPIEndpoint(BaseAPIView):
filter=Q(
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -88,6 +84,7 @@ class CycleAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -98,6 +95,7 @@ class CycleAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="cancelled",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -108,6 +106,7 @@ class CycleAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -118,6 +117,7 @@ class CycleAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="unstarted",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -128,6 +128,7 @@ class CycleAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="backlog",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -137,26 +138,18 @@ class CycleAPIEndpoint(BaseAPIView):
def get(self, request, slug, project_id, pk=None):
if pk:
queryset = (
self.get_queryset().filter(archived_at__isnull=True).get(pk=pk)
)
queryset = self.get_queryset().filter(archived_at__isnull=True).get(pk=pk)
data = CycleSerializer(
queryset,
fields=self.fields,
expand=self.expand,
queryset, fields=self.fields, expand=self.expand
).data
return Response(
data,
status=status.HTTP_200_OK,
)
return Response(data, status=status.HTTP_200_OK)
queryset = self.get_queryset().filter(archived_at__isnull=True)
cycle_view = request.GET.get("cycle_view", "all")
# Current Cycle
if cycle_view == "current":
queryset = queryset.filter(
start_date__lte=timezone.now(),
end_date__gte=timezone.now(),
start_date__lte=timezone.now(), end_date__gte=timezone.now()
)
data = CycleSerializer(
queryset, many=True, fields=self.fields, expand=self.expand
@@ -170,10 +163,7 @@ class CycleAPIEndpoint(BaseAPIView):
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles,
many=True,
fields=self.fields,
expand=self.expand,
cycles, many=True, fields=self.fields, expand=self.expand
).data,
)
@@ -184,53 +174,38 @@ class CycleAPIEndpoint(BaseAPIView):
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles,
many=True,
fields=self.fields,
expand=self.expand,
cycles, many=True, fields=self.fields, expand=self.expand
).data,
)
# Draft Cycles
if cycle_view == "draft":
queryset = queryset.filter(
end_date=None,
start_date=None,
)
queryset = queryset.filter(end_date=None, start_date=None)
return self.paginate(
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles,
many=True,
fields=self.fields,
expand=self.expand,
cycles, many=True, fields=self.fields, expand=self.expand
).data,
)
# Incomplete Cycles
if cycle_view == "incomplete":
queryset = queryset.filter(
Q(end_date__gte=timezone.now()) | Q(end_date__isnull=True),
Q(end_date__gte=timezone.now()) | Q(end_date__isnull=True)
)
return self.paginate(
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles,
many=True,
fields=self.fields,
expand=self.expand,
cycles, many=True, fields=self.fields, expand=self.expand
).data,
)
return self.paginate(
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles,
many=True,
fields=self.fields,
expand=self.expand,
cycles, many=True, fields=self.fields, expand=self.expand
).data,
)
@@ -267,10 +242,7 @@ class CycleAPIEndpoint(BaseAPIView):
},
status=status.HTTP_409_CONFLICT,
)
serializer.save(
project_id=project_id,
owned_by=request.user,
)
serializer.save(project_id=project_id, owned_by=request.user)
# Send the model activity
model_activity.delay(
model_name="cycle",
@@ -281,12 +253,8 @@ class CycleAPIEndpoint(BaseAPIView):
slug=slug,
origin=request.META.get("HTTP_ORIGIN"),
)
return Response(
serializer.data, status=status.HTTP_201_CREATED
)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else:
return Response(
{
@@ -296,9 +264,7 @@ class CycleAPIEndpoint(BaseAPIView):
)
def patch(self, request, slug, project_id, pk):
cycle = Cycle.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
current_instance = json.dumps(
CycleSerializer(cycle).data, cls=DjangoJSONEncoder
@@ -316,9 +282,7 @@ class CycleAPIEndpoint(BaseAPIView):
if "sort_order" in request_data:
# Can only change sort order
request_data = {
"sort_order": request_data.get(
"sort_order", cycle.sort_order
)
"sort_order": request_data.get("sort_order", cycle.sort_order)
}
else:
return Response(
@@ -365,9 +329,7 @@ class CycleAPIEndpoint(BaseAPIView):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, slug, project_id, pk):
cycle = Cycle.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
if cycle.owned_by_id != request.user.id and (
not ProjectMember.objects.filter(
workspace__slug=slug,
@@ -383,9 +345,9 @@ class CycleAPIEndpoint(BaseAPIView):
)
cycle_issues = list(
CycleIssue.objects.filter(
cycle_id=self.kwargs.get("pk")
).values_list("issue", flat=True)
CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
"issue", flat=True
)
)
issue_activity.delay(
@@ -404,20 +366,16 @@ class CycleAPIEndpoint(BaseAPIView):
epoch=int(timezone.now().timestamp()),
)
# Delete the cycle
cycle.delete(soft=False)
cycle.delete()
# Delete the user favorite cycle
UserFavorite.objects.filter(
entity_type="cycle",
entity_identifier=pk,
project_id=project_id,
entity_type="cycle", entity_identifier=pk, project_id=project_id
).delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
permission_classes = [ProjectEntityPermission]
def get_queryset(self):
return (
@@ -437,6 +395,7 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
filter=Q(
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -447,6 +406,7 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -457,6 +417,7 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="cancelled",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -467,6 +428,7 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -477,6 +439,7 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="unstarted",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -487,12 +450,11 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="backlog",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
.annotate(
total_estimates=Sum("issue_cycle__issue__estimate_point")
)
.annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
.annotate(
completed_estimates=Sum(
"issue_cycle__issue__estimate_point",
@@ -500,6 +462,7 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -510,6 +473,7 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -522,10 +486,7 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
request=request,
queryset=(self.get_queryset()),
on_results=lambda cycles: CycleSerializer(
cycles,
many=True,
fields=self.fields,
expand=self.expand,
cycles, many=True, fields=self.fields, expand=self.expand
).data,
)
@@ -568,16 +529,12 @@ class CycleIssueAPIEndpoint(BaseAPIView):
model = CycleIssue
webhook_event = "cycle_issue"
bulk = True
permission_classes = [
ProjectEntityPermission,
]
permission_classes = [ProjectEntityPermission]
def get_queryset(self):
return (
CycleIssue.objects.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("issue_id")
)
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue_id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -615,11 +572,11 @@ class CycleIssueAPIEndpoint(BaseAPIView):
# List
order_by = request.GET.get("order_by", "created_at")
issues = (
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id, issue_cycle__deleted_at__isnull=True
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -655,10 +612,7 @@ class CycleIssueAPIEndpoint(BaseAPIView):
request=request,
queryset=(issues),
on_results=lambda issues: CycleSerializer(
issues,
many=True,
fields=self.fields,
expand=self.expand,
issues, many=True, fields=self.fields, expand=self.expand
).data,
)
@@ -667,8 +621,7 @@ class CycleIssueAPIEndpoint(BaseAPIView):
if not issues:
return Response(
{"error": "Issues are required"},
status=status.HTTP_400_BAD_REQUEST,
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
)
cycle = Cycle.objects.get(
@@ -677,9 +630,7 @@ class CycleIssueAPIEndpoint(BaseAPIView):
# Get all CycleIssues already created
cycle_issues = list(
CycleIssue.objects.filter(
~Q(cycle_id=cycle_id), issue_id__in=issues
)
CycleIssue.objects.filter(~Q(cycle_id=cycle_id), issue_id__in=issues)
)
existing_issues = [
@@ -724,9 +675,7 @@ class CycleIssueAPIEndpoint(BaseAPIView):
)
# Update the cycle issues
CycleIssue.objects.bulk_update(
updated_records, ["cycle_id"], batch_size=100
)
CycleIssue.objects.bulk_update(updated_records, ["cycle_id"], batch_size=100)
# Capture Issue Activity
issue_activity.delay(
@@ -785,9 +734,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
"""
permission_classes = [
ProjectEntityPermission,
]
permission_classes = [ProjectEntityPermission]
def post(self, request, slug, project_id, cycle_id):
new_cycle_id = request.data.get("new_cycle_id", False)
@@ -812,6 +759,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
filter=Q(
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -822,6 +770,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -832,6 +781,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="cancelled",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -842,6 +792,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -852,6 +803,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="unstarted",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -862,6 +814,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="backlog",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -878,6 +831,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
assignee_estimate_data = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
@@ -906,9 +860,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
)
.values("display_name", "assignee_id", "avatar", "avatar_url")
.annotate(
total_estimates=Sum(
Cast("estimate_point__value", FloatField())
)
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
)
.annotate(
completed_estimates=Sum(
@@ -937,9 +889,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
{
"display_name": item["display_name"],
"assignee_id": (
str(item["assignee_id"])
if item["assignee_id"]
else None
str(item["assignee_id"]) if item["assignee_id"] else None
),
"avatar": item.get("avatar", None),
"avatar_url": item.get("avatar_url", None),
@@ -953,6 +903,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
label_distribution_data = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
@@ -961,9 +912,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
.annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id")
.annotate(
total_estimates=Sum(
Cast("estimate_point__value", FloatField())
)
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
)
.annotate(
completed_estimates=Sum(
@@ -1000,9 +949,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
{
"label_name": item["label_name"],
"color": item["color"],
"label_id": (
str(item["label_id"]) if item["label_id"] else None
),
"label_id": (str(item["label_id"]) if item["label_id"] else None),
"total_estimates": item["total_estimates"],
"completed_estimates": item["completed_estimates"],
"pending_estimates": item["pending_estimates"],
@@ -1014,6 +961,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
assignee_distribution = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
@@ -1033,8 +981,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
),
# If `avatar_asset` is None, fall back to using `avatar` field directly
When(
assignees__avatar_asset__isnull=True,
then="assignees__avatar",
assignees__avatar_asset__isnull=True, then="assignees__avatar"
),
default=Value(None),
output_field=models.CharField(),
@@ -1043,12 +990,8 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
.values("display_name", "assignee_id", "avatar_url")
.annotate(
total_issues=Count(
"id",
filter=Q(
archived_at__isnull=True,
is_draft=False,
),
),
"id", filter=Q(archived_at__isnull=True, is_draft=False)
)
)
.annotate(
completed_issues=Count(
@@ -1092,6 +1035,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
label_distribution = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
@@ -1101,12 +1045,8 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
.values("label_name", "color", "label_id")
.annotate(
total_issues=Count(
"id",
filter=Q(
archived_at__isnull=True,
is_draft=False,
),
),
"id", filter=Q(archived_at__isnull=True, is_draft=False)
)
)
.annotate(
completed_issues=Count(
@@ -1136,9 +1076,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
{
"label_name": item["label_name"],
"color": item["color"],
"label_id": (
str(item["label_id"]) if item["label_id"] else None
),
"label_id": (str(item["label_id"]) if item["label_id"] else None),
"total_issues": item["total_issues"],
"completed_issues": item["completed_issues"],
"pending_issues": item["pending_issues"],
@@ -1183,10 +1121,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
}
current_cycle.save(update_fields=["progress_snapshot"])
if (
new_cycle.end_date is not None
and new_cycle.end_date < timezone.now()
):
if new_cycle.end_date is not None and new_cycle.end_date < timezone.now():
return Response(
{
"error": "The cycle where the issues are transferred is already completed"
@@ -14,60 +14,47 @@ from rest_framework import status
from rest_framework.response import Response
# Module imports
from plane.api.serializers import InboxIssueSerializer, IssueSerializer
from plane.api.serializers import IntakeIssueSerializer, IssueSerializer
from plane.app.permissions import ProjectLitePermission
from plane.bgtasks.issue_activities_task import issue_activity
from plane.db.models import (
Inbox,
InboxIssue,
Issue,
Project,
ProjectMember,
State,
)
from plane.db.models import Intake, IntakeIssue, Issue, Project, ProjectMember, State
from .base import BaseAPIView
class InboxIssueAPIEndpoint(BaseAPIView):
class IntakeIssueAPIEndpoint(BaseAPIView):
"""
This viewset automatically provides `list`, `create`, `retrieve`,
`update` and `destroy` actions related to inbox issues.
`update` and `destroy` actions related to intake issues.
"""
permission_classes = [
ProjectLitePermission,
]
permission_classes = [ProjectLitePermission]
serializer_class = InboxIssueSerializer
model = InboxIssue
serializer_class = IntakeIssueSerializer
model = IntakeIssue
filterset_fields = [
"status",
]
filterset_fields = ["status"]
def get_queryset(self):
inbox = Inbox.objects.filter(
intake = Intake.objects.filter(
workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"),
).first()
project = Project.objects.get(
workspace__slug=self.kwargs.get("slug"),
pk=self.kwargs.get("project_id"),
workspace__slug=self.kwargs.get("slug"), pk=self.kwargs.get("project_id")
)
if inbox is None and not project.inbox_view:
return InboxIssue.objects.none()
if intake is None and not project.intake_view:
return IntakeIssue.objects.none()
return (
InboxIssue.objects.filter(
Q(snoozed_till__gte=timezone.now())
| Q(snoozed_till__isnull=True),
IntakeIssue.objects.filter(
Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"),
inbox_id=inbox.id,
intake_id=intake.id,
)
.select_related("issue", "workspace", "project")
.order_by(self.kwargs.get("order_by", "-created_at"))
@@ -75,49 +62,37 @@ class InboxIssueAPIEndpoint(BaseAPIView):
def get(self, request, slug, project_id, issue_id=None):
if issue_id:
inbox_issue_queryset = self.get_queryset().get(issue_id=issue_id)
inbox_issue_data = InboxIssueSerializer(
inbox_issue_queryset,
fields=self.fields,
expand=self.expand,
intake_issue_queryset = self.get_queryset().get(issue_id=issue_id)
intake_issue_data = IntakeIssueSerializer(
intake_issue_queryset, fields=self.fields, expand=self.expand
).data
return Response(
inbox_issue_data,
status=status.HTTP_200_OK,
)
return Response(intake_issue_data, status=status.HTTP_200_OK)
issue_queryset = self.get_queryset()
return self.paginate(
request=request,
queryset=(issue_queryset),
on_results=lambda inbox_issues: InboxIssueSerializer(
inbox_issues,
many=True,
fields=self.fields,
expand=self.expand,
on_results=lambda intake_issues: IntakeIssueSerializer(
intake_issues, many=True, fields=self.fields, expand=self.expand
).data,
)
def post(self, request, slug, project_id):
if not request.data.get("issue", {}).get("name", False):
return Response(
{"error": "Name is required"},
status=status.HTTP_400_BAD_REQUEST,
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
)
inbox = Inbox.objects.filter(
intake = Intake.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
project = Project.objects.get(
workspace__slug=slug,
pk=project_id,
)
project = Project.objects.get(workspace__slug=slug, pk=project_id)
# Inbox view
if inbox is None and not project.inbox_view:
# Intake view
if intake is None and not project.intake_view:
return Response(
{
"error": "Inbox is not enabled for this project enable it through the project's api"
"error": "Intake is not enabled for this project enable it through the project's api"
},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -131,15 +106,14 @@ class InboxIssueAPIEndpoint(BaseAPIView):
"none",
]:
return Response(
{"error": "Invalid priority"},
status=status.HTTP_400_BAD_REQUEST,
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
)
# Create or get state
state, _ = State.objects.get_or_create(
name="Triage",
group="triage",
description="Default state for managing all Inbox Issues",
description="Default state for managing all Intake Issues",
project_id=project_id,
color="#ff7700",
is_triage=True,
@@ -157,12 +131,12 @@ class InboxIssueAPIEndpoint(BaseAPIView):
state=state,
)
# create an inbox issue
inbox_issue = InboxIssue.objects.create(
inbox_id=inbox.id,
# create an intake issue
intake_issue = IntakeIssue.objects.create(
intake_id=intake.id,
project_id=project_id,
issue=issue,
source=request.data.get("source", "in-app"),
source=request.data.get("source", "IN-APP"),
)
# Create an Issue Activity
issue_activity.delay(
@@ -173,32 +147,34 @@ class InboxIssueAPIEndpoint(BaseAPIView):
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
inbox=str(inbox_issue.id),
intake=str(intake_issue.id),
)
serializer = InboxIssueSerializer(inbox_issue)
serializer = IntakeIssueSerializer(intake_issue)
return Response(serializer.data, status=status.HTTP_200_OK)
def patch(self, request, slug, project_id, issue_id):
inbox = Inbox.objects.filter(
intake = Intake.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
# Inbox view
if inbox is None:
project = Project.objects.get(workspace__slug=slug, pk=project_id)
# Intake view
if intake is None and not project.intake_view:
return Response(
{
"error": "Inbox is not enabled for this project enable it through the project's api"
"error": "Intake is not enabled for this project enable it through the project's api"
},
status=status.HTTP_400_BAD_REQUEST,
)
# Get the inbox issue
inbox_issue = InboxIssue.objects.get(
# Get the intake issue
intake_issue = IntakeIssue.objects.get(
issue_id=issue_id,
workspace__slug=slug,
project_id=project_id,
inbox_id=inbox.id,
intake_id=intake.id,
)
# Get the project member
@@ -210,11 +186,11 @@ class InboxIssueAPIEndpoint(BaseAPIView):
)
# Only project members admins and created_by users can access this endpoint
if project_member.role <= 5 and str(inbox_issue.created_by_id) != str(
if project_member.role <= 5 and str(intake_issue.created_by_id) != str(
request.user.id
):
return Response(
{"error": "You cannot edit inbox issues"},
{"error": "You cannot edit intake issues"},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -227,7 +203,10 @@ class InboxIssueAPIEndpoint(BaseAPIView):
ArrayAgg(
"labels__id",
distinct=True,
filter=~Q(labels__id__isnull=True),
filter=Q(
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -235,15 +214,15 @@ class InboxIssueAPIEndpoint(BaseAPIView):
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True),
filter=Q(
~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True)
& Q(issue_assignee__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
).get(
pk=issue_id,
workspace__slug=slug,
project_id=project_id,
)
).get(pk=issue_id, workspace__slug=slug, project_id=project_id)
# Only allow guests to edit name and description
if project_member.role <= 5:
issue_data = {
@@ -251,14 +230,10 @@ class InboxIssueAPIEndpoint(BaseAPIView):
"description_html": issue_data.get(
"description_html", issue.description_html
),
"description": issue_data.get(
"description", issue.description
),
"description": issue_data.get("description", issue.description),
}
issue_serializer = IssueSerializer(
issue, data=issue_data, partial=True
)
issue_serializer = IssueSerializer(issue, data=issue_data, partial=True)
if issue_serializer.is_valid():
current_instance = issue
@@ -276,7 +251,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
cls=DjangoJSONEncoder,
),
epoch=int(timezone.now().timestamp()),
inbox=(inbox_issue.id),
intake=(intake_issue.id),
)
issue_serializer.save()
else:
@@ -284,13 +259,13 @@ class InboxIssueAPIEndpoint(BaseAPIView):
issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
# Only project admins and members can edit inbox issue attributes
# Only project admins and members can edit intake issue attributes
if project_member.role > 15:
serializer = InboxIssueSerializer(
inbox_issue, data=request.data, partial=True
serializer = IntakeIssueSerializer(
intake_issue, data=request.data, partial=True
)
current_instance = json.dumps(
InboxIssueSerializer(inbox_issue).data, cls=DjangoJSONEncoder
IntakeIssueSerializer(intake_issue).data, cls=DjangoJSONEncoder
)
if serializer.is_valid():
@@ -298,14 +273,10 @@ class InboxIssueAPIEndpoint(BaseAPIView):
# Update the issue state if the issue is rejected or marked as duplicate
if serializer.data["status"] in [-1, 2]:
issue = Issue.objects.get(
pk=issue_id,
workspace__slug=slug,
project_id=project_id,
pk=issue_id, workspace__slug=slug, project_id=project_id
)
state = State.objects.filter(
group="cancelled",
workspace__slug=slug,
project_id=project_id,
group="cancelled", workspace__slug=slug, project_id=project_id
).first()
if state is not None:
issue.state = state
@@ -314,18 +285,14 @@ class InboxIssueAPIEndpoint(BaseAPIView):
# Update the issue state if it is accepted
if serializer.data["status"] in [1]:
issue = Issue.objects.get(
pk=issue_id,
workspace__slug=slug,
project_id=project_id,
pk=issue_id, workspace__slug=slug, project_id=project_id
)
# Update the issue state only if it is in triage state
if issue.state.is_triage:
# Move to default state
state = State.objects.filter(
workspace__slug=slug,
project_id=project_id,
default=True,
workspace__slug=slug, project_id=project_id, default=True
).first()
if state is not None:
issue.state = state
@@ -333,10 +300,8 @@ class InboxIssueAPIEndpoint(BaseAPIView):
# create a activity for status change
issue_activity.delay(
type="inbox.activity.created",
requested_data=json.dumps(
request.data, cls=DjangoJSONEncoder
),
type="intake.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),
@@ -344,48 +309,42 @@ class InboxIssueAPIEndpoint(BaseAPIView):
epoch=int(timezone.now().timestamp()),
notification=False,
origin=request.META.get("HTTP_ORIGIN"),
inbox=str(inbox_issue.id),
intake=str(intake_issue.id),
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else:
return Response(
InboxIssueSerializer(inbox_issue).data,
status=status.HTTP_200_OK,
IntakeIssueSerializer(intake_issue).data, status=status.HTTP_200_OK
)
def delete(self, request, slug, project_id, issue_id):
inbox = Inbox.objects.filter(
intake = Intake.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
project = Project.objects.get(
workspace__slug=slug,
pk=project_id,
)
project = Project.objects.get(workspace__slug=slug, pk=project_id)
# Inbox view
if inbox is None and not project.inbox_view:
# Intake view
if intake is None and not project.intake_view:
return Response(
{
"error": "Inbox is not enabled for this project enable it through the project's api"
"error": "Intake is not enabled for this project enable it through the project's api"
},
status=status.HTTP_400_BAD_REQUEST,
)
# Get the inbox issue
inbox_issue = InboxIssue.objects.get(
# Get the intake issue
intake_issue = IntakeIssue.objects.get(
issue_id=issue_id,
workspace__slug=slug,
project_id=project_id,
inbox_id=inbox.id,
intake_id=intake.id,
)
# Check the issue status
if inbox_issue.status in [-2, -1, 0, 2]:
if intake_issue.status in [-2, -1, 0, 2]:
# Delete the issue also
issue = Issue.objects.filter(
workspace__slug=slug, project_id=project_id, pk=issue_id
@@ -405,5 +364,5 @@ class InboxIssueAPIEndpoint(BaseAPIView):
)
issue.delete()
inbox_issue.delete()
intake_issue.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
+63 -198
View File
@@ -16,6 +16,7 @@ from django.db.models import (
Q,
Value,
When,
Subquery,
)
from django.utils import timezone
@@ -48,6 +49,7 @@ from plane.db.models import (
Label,
Project,
ProjectMember,
CycleIssue,
)
from .base import BaseAPIView
@@ -71,9 +73,7 @@ class WorkspaceIssueAPIEndpoint(BaseAPIView):
def get_queryset(self):
return (
Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -89,14 +89,10 @@ class WorkspaceIssueAPIEndpoint(BaseAPIView):
.order_by(self.kwargs.get("order_by", "-created_at"))
).distinct()
def get(
self, request, slug, project__identifier=None, issue__identifier=None
):
def get(self, request, slug, project__identifier=None, issue__identifier=None):
if issue__identifier and project__identifier:
issue = Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -106,11 +102,7 @@ class WorkspaceIssueAPIEndpoint(BaseAPIView):
sequence_id=issue__identifier,
)
return Response(
IssueSerializer(
issue,
fields=self.fields,
expand=self.expand,
).data,
IssueSerializer(issue, fields=self.fields, expand=self.expand).data,
status=status.HTTP_200_OK,
)
@@ -124,17 +116,13 @@ class IssueAPIEndpoint(BaseAPIView):
model = Issue
webhook_event = "issue"
permission_classes = [
ProjectEntityPermission,
]
permission_classes = [ProjectEntityPermission]
serializer_class = IssueSerializer
def get_queryset(self):
return (
Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -162,53 +150,35 @@ class IssueAPIEndpoint(BaseAPIView):
project_id=project_id,
)
return Response(
IssueSerializer(
issue,
fields=self.fields,
expand=self.expand,
).data,
IssueSerializer(issue, fields=self.fields, expand=self.expand).data,
status=status.HTTP_200_OK,
)
if pk:
issue = Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
).get(workspace__slug=slug, project_id=project_id, pk=pk)
return Response(
IssueSerializer(
issue,
fields=self.fields,
expand=self.expand,
).data,
IssueSerializer(issue, fields=self.fields, expand=self.expand).data,
status=status.HTTP_200_OK,
)
# Custom ordering for priority and state
priority_order = ["urgent", "high", "medium", "low", "none"]
state_order = [
"backlog",
"unstarted",
"started",
"completed",
"cancelled",
]
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
order_by_param = request.GET.get("order_by", "-created_at")
issue_queryset = (
self.get_queryset()
.annotate(
cycle_id=Case(
When(
Q(issue_cycle__cycle__deleted_at__isnull=True),
then=F("issue_cycle__cycle_id"),
),
default=None,
cycle_id=Subquery(
CycleIssue.objects.filter(
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
)
)
.annotate(
@@ -231,9 +201,7 @@ class IssueAPIEndpoint(BaseAPIView):
# Priority Ordering
if order_by_param == "priority" or order_by_param == "-priority":
priority_order = (
priority_order
if order_by_param == "priority"
else priority_order[::-1]
priority_order if order_by_param == "priority" else priority_order[::-1]
)
issue_queryset = issue_queryset.annotate(
priority_order=Case(
@@ -281,9 +249,7 @@ class IssueAPIEndpoint(BaseAPIView):
else order_by_param
)
).order_by(
"-max_values"
if order_by_param.startswith("-")
else "max_values"
"-max_values" if order_by_param.startswith("-") else "max_values"
)
else:
issue_queryset = issue_queryset.order_by(order_by_param)
@@ -292,10 +258,7 @@ class IssueAPIEndpoint(BaseAPIView):
request=request,
queryset=(issue_queryset),
on_results=lambda issues: IssueSerializer(
issues,
many=True,
fields=self.fields,
expand=self.expand,
issues, many=True, fields=self.fields, expand=self.expand
).data,
)
@@ -339,22 +302,16 @@ class IssueAPIEndpoint(BaseAPIView):
serializer.save()
# Refetch the issue
issue = Issue.objects.filter(
workspace__slug=slug,
project_id=project_id,
pk=serializer.data["id"],
workspace__slug=slug, project_id=project_id, pk=serializer.data["id"]
).first()
issue.created_at = request.data.get("created_at", timezone.now())
issue.created_by_id = request.data.get(
"created_by", request.user.id
)
issue.created_by_id = request.data.get("created_by", request.user.id)
issue.save(update_fields=["created_at", "created_by"])
# Track the issue
issue_activity.delay(
type="issue.activity.created",
requested_data=json.dumps(
self.request.data, cls=DjangoJSONEncoder
),
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),
@@ -391,9 +348,7 @@ class IssueAPIEndpoint(BaseAPIView):
# Get the requested data, encode it as django object and pass it
# to serializer to validation
requested_data = json.dumps(
self.request.data, cls=DjangoJSONEncoder
)
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
serializer = IssueSerializer(
issue,
data=request.data,
@@ -451,9 +406,7 @@ class IssueAPIEndpoint(BaseAPIView):
# If any of the created_at or created_by is present, update
# the issue with the provided data, else return with the
# default states given.
issue.created_at = request.data.get(
"created_at", timezone.now()
)
issue.created_at = request.data.get("created_at", timezone.now())
issue.created_by_id = request.data.get(
"created_by", request.user.id
)
@@ -470,12 +423,8 @@ class IssueAPIEndpoint(BaseAPIView):
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
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else:
return Response(
{"error": "external_id and external_source are required"},
@@ -483,9 +432,7 @@ class IssueAPIEndpoint(BaseAPIView):
)
def patch(self, request, slug, project_id, pk=None):
issue = Issue.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
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
@@ -494,10 +441,7 @@ class IssueAPIEndpoint(BaseAPIView):
serializer = IssueSerializer(
issue,
data=request.data,
context={
"project_id": project_id,
"workspace_id": project.workspace_id,
},
context={"project_id": project_id, "workspace_id": project.workspace_id},
partial=True,
)
if serializer.is_valid():
@@ -535,9 +479,7 @@ class IssueAPIEndpoint(BaseAPIView):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, slug, project_id, pk=None):
issue = Issue.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
if issue.created_by_id != request.user.id and (
not ProjectMember.objects.filter(
workspace__slug=slug,
@@ -576,9 +518,7 @@ class LabelAPIEndpoint(BaseAPIView):
serializer_class = LabelSerializer
model = Label
permission_classes = [
ProjectMemberPermission,
]
permission_classes = [ProjectMemberPermission]
def get_queryset(self):
return (
@@ -625,12 +565,8 @@ class LabelAPIEndpoint(BaseAPIView):
)
serializer.save(project_id=project_id)
return Response(
serializer.data, status=status.HTTP_201_CREATED
)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError:
label = Label.objects.filter(
workspace__slug=slug,
@@ -651,18 +587,11 @@ class LabelAPIEndpoint(BaseAPIView):
request=request,
queryset=(self.get_queryset()),
on_results=lambda labels: LabelSerializer(
labels,
many=True,
fields=self.fields,
expand=self.expand,
labels, many=True, fields=self.fields, expand=self.expand
).data,
)
label = self.get_queryset().get(pk=pk)
serializer = LabelSerializer(
label,
fields=self.fields,
expand=self.expand,
)
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):
@@ -705,9 +634,7 @@ class IssueLinkAPIEndpoint(BaseAPIView):
"""
permission_classes = [
ProjectEntityPermission,
]
permission_classes = [ProjectEntityPermission]
model = IssueLink
serializer_class = IssueLinkSerializer
@@ -730,46 +657,32 @@ class IssueLinkAPIEndpoint(BaseAPIView):
if pk is None:
issue_links = self.get_queryset()
serializer = IssueLinkSerializer(
issue_links,
fields=self.fields,
expand=self.expand,
issue_links, fields=self.fields, expand=self.expand
)
return self.paginate(
request=request,
queryset=(self.get_queryset()),
on_results=lambda issue_links: IssueLinkSerializer(
issue_links,
many=True,
fields=self.fields,
expand=self.expand,
issue_links, many=True, fields=self.fields, expand=self.expand
).data,
)
issue_link = self.get_queryset().get(pk=pk)
serializer = IssueLinkSerializer(
issue_link,
fields=self.fields,
expand=self.expand,
issue_link, fields=self.fields, expand=self.expand
)
return Response(serializer.data, status=status.HTTP_200_OK)
def post(self, request, slug, project_id, issue_id):
serializer = IssueLinkSerializer(data=request.data)
if serializer.is_valid():
serializer.save(
project_id=project_id,
issue_id=issue_id,
)
serializer.save(project_id=project_id, issue_id=issue_id)
link = IssueLink.objects.get(pk=serializer.data["id"])
link.created_by_id = request.data.get(
"created_by", request.user.id
)
link.created_by_id = request.data.get("created_by", request.user.id)
link.save(update_fields=["created_by"])
issue_activity.delay(
type="link.activity.created",
requested_data=json.dumps(
serializer.data, cls=DjangoJSONEncoder
),
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
issue_id=str(self.kwargs.get("issue_id")),
project_id=str(self.kwargs.get("project_id")),
actor_id=str(link.created_by_id),
@@ -781,19 +694,13 @@ class IssueLinkAPIEndpoint(BaseAPIView):
def patch(self, request, slug, project_id, issue_id, pk):
issue_link = IssueLink.objects.get(
workspace__slug=slug,
project_id=project_id,
issue_id=issue_id,
pk=pk,
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
)
requested_data = json.dumps(request.data, cls=DjangoJSONEncoder)
current_instance = json.dumps(
IssueLinkSerializer(issue_link).data,
cls=DjangoJSONEncoder,
)
serializer = IssueLinkSerializer(
issue_link, data=request.data, partial=True
IssueLinkSerializer(issue_link).data, cls=DjangoJSONEncoder
)
serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
issue_activity.delay(
@@ -810,14 +717,10 @@ class IssueLinkAPIEndpoint(BaseAPIView):
def delete(self, request, slug, project_id, issue_id, pk):
issue_link = IssueLink.objects.get(
workspace__slug=slug,
project_id=project_id,
issue_id=issue_id,
pk=pk,
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
)
current_instance = json.dumps(
IssueLinkSerializer(issue_link).data,
cls=DjangoJSONEncoder,
IssueLinkSerializer(issue_link).data, cls=DjangoJSONEncoder
)
issue_activity.delay(
type="link.activity.deleted",
@@ -842,15 +745,11 @@ class IssueCommentAPIEndpoint(BaseAPIView):
serializer_class = IssueCommentSerializer
model = IssueComment
webhook_event = "issue_comment"
permission_classes = [
ProjectLitePermission,
]
permission_classes = [ProjectLitePermission]
def get_queryset(self):
return (
IssueComment.objects.filter(
workspace__slug=self.kwargs.get("slug")
)
IssueComment.objects.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(issue_id=self.kwargs.get("issue_id"))
.filter(
@@ -877,19 +776,14 @@ class IssueCommentAPIEndpoint(BaseAPIView):
if pk:
issue_comment = self.get_queryset().get(pk=pk)
serializer = IssueCommentSerializer(
issue_comment,
fields=self.fields,
expand=self.expand,
issue_comment, fields=self.fields, expand=self.expand
)
return Response(serializer.data, status=status.HTTP_200_OK)
return self.paginate(
request=request,
queryset=(self.get_queryset()),
on_results=lambda issue_comment: IssueCommentSerializer(
issue_comment,
many=True,
fields=self.fields,
expand=self.expand,
issue_comment, many=True, fields=self.fields, expand=self.expand
).data,
)
@@ -922,17 +816,11 @@ class IssueCommentAPIEndpoint(BaseAPIView):
serializer = IssueCommentSerializer(data=request.data)
if serializer.is_valid():
serializer.save(
project_id=project_id,
issue_id=issue_id,
actor=request.user,
)
issue_comment = IssueComment.objects.get(
pk=serializer.data.get("id")
project_id=project_id, issue_id=issue_id, actor=request.user
)
issue_comment = IssueComment.objects.get(pk=serializer.data.get("id"))
# Update the created_at and the created_by and save the comment
issue_comment.created_at = request.data.get(
"created_at", timezone.now()
)
issue_comment.created_at = request.data.get("created_at", timezone.now())
issue_comment.created_by_id = request.data.get(
"created_by", request.user.id
)
@@ -940,9 +828,7 @@ class IssueCommentAPIEndpoint(BaseAPIView):
issue_activity.delay(
type="comment.activity.created",
requested_data=json.dumps(
serializer.data, cls=DjangoJSONEncoder
),
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
actor_id=str(issue_comment.created_by_id),
issue_id=str(self.kwargs.get("issue_id")),
project_id=str(self.kwargs.get("project_id")),
@@ -954,24 +840,17 @@ class IssueCommentAPIEndpoint(BaseAPIView):
def patch(self, request, slug, project_id, issue_id, pk):
issue_comment = IssueComment.objects.get(
workspace__slug=slug,
project_id=project_id,
issue_id=issue_id,
pk=pk,
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
)
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
current_instance = json.dumps(
IssueCommentSerializer(issue_comment).data,
cls=DjangoJSONEncoder,
IssueCommentSerializer(issue_comment).data, cls=DjangoJSONEncoder
)
# Validation check if the issue already exists
if (
request.data.get("external_id")
and (
issue_comment.external_id
!= str(request.data.get("external_id"))
)
and (issue_comment.external_id != str(request.data.get("external_id")))
and IssueComment.objects.filter(
project_id=project_id,
workspace__slug=slug,
@@ -1008,14 +887,10 @@ class IssueCommentAPIEndpoint(BaseAPIView):
def delete(self, request, slug, project_id, issue_id, pk):
issue_comment = IssueComment.objects.get(
workspace__slug=slug,
project_id=project_id,
issue_id=issue_id,
pk=pk,
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
)
current_instance = json.dumps(
IssueCommentSerializer(issue_comment).data,
cls=DjangoJSONEncoder,
IssueCommentSerializer(issue_comment).data, cls=DjangoJSONEncoder
)
issue_comment.delete()
issue_activity.delay(
@@ -1031,9 +906,7 @@ class IssueCommentAPIEndpoint(BaseAPIView):
class IssueActivityAPIEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
permission_classes = [ProjectEntityPermission]
def get(self, request, slug, project_id, issue_id, pk=None):
issue_activities = (
@@ -1058,19 +931,14 @@ class IssueActivityAPIEndpoint(BaseAPIView):
request=request,
queryset=(issue_activities),
on_results=lambda issue_activity: IssueActivitySerializer(
issue_activity,
many=True,
fields=self.fields,
expand=self.expand,
issue_activity, many=True, fields=self.fields, expand=self.expand
).data,
)
class IssueAttachmentEndpoint(BaseAPIView):
serializer_class = IssueAttachmentSerializer
permission_classes = [
ProjectEntityPermission,
]
permission_classes = [ProjectEntityPermission]
model = FileAsset
parser_classes = (MultiPartParser, FormParser)
@@ -1109,10 +977,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
serializer.data,
cls=DjangoJSONEncoder,
),
current_instance=json.dumps(serializer.data, cls=DjangoJSONEncoder),
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
+8 -29
View File
@@ -13,24 +13,14 @@ from rest_framework import status
# Module imports
from .base import BaseAPIView
from plane.api.serializers import UserLiteSerializer
from plane.db.models import (
User,
Workspace,
Project,
WorkspaceMember,
ProjectMember,
)
from plane.db.models import User, Workspace, Project, WorkspaceMember, ProjectMember
from plane.app.permissions import (
ProjectMemberPermission,
)
from plane.app.permissions import ProjectMemberPermission
# API endpoint to get and insert users inside the workspace
class ProjectMemberAPIEndpoint(BaseAPIView):
permission_classes = [
ProjectMemberPermission,
]
permission_classes = [ProjectMemberPermission]
# Get all the users that are present inside the workspace
def get(self, request, slug, project_id):
@@ -48,10 +38,7 @@ class ProjectMemberAPIEndpoint(BaseAPIView):
# Get all the users that are present inside the workspace
users = UserLiteSerializer(
User.objects.filter(
id__in=project_members,
),
many=True,
User.objects.filter(id__in=project_members), many=True
).data
return Response(users, status=status.HTTP_200_OK)
@@ -78,8 +65,7 @@ class ProjectMemberAPIEndpoint(BaseAPIView):
validate_email(email)
except ValidationError:
return Response(
{"error": "Invalid email provided"},
status=status.HTTP_400_BAD_REQUEST,
{"error": "Invalid email provided"}, status=status.HTTP_400_BAD_REQUEST
)
workspace = Workspace.objects.filter(slug=slug).first()
@@ -108,9 +94,7 @@ class ProjectMemberAPIEndpoint(BaseAPIView):
).first()
if project_member:
return Response(
{
"error": "User is already part of the workspace and project"
},
{"error": "User is already part of the workspace and project"},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -131,18 +115,14 @@ class ProjectMemberAPIEndpoint(BaseAPIView):
# Create a workspace member for the user if not already a member
if not workspace_member:
workspace_member = WorkspaceMember.objects.create(
workspace=workspace,
member=user,
role=request.data.get("role", 5),
workspace=workspace, member=user, role=request.data.get("role", 5)
)
workspace_member.save()
# Create a project member for the user if not already a member
if not project_member:
project_member = ProjectMember.objects.create(
project=project,
member=user,
role=request.data.get("role", 5),
project=project, member=user, role=request.data.get("role", 5)
)
project_member.save()
@@ -150,4 +130,3 @@ class ProjectMemberAPIEndpoint(BaseAPIView):
user_data = UserLiteSerializer(user).data
return Response(user_data, status=status.HTTP_201_CREATED)
+45 -98
View File
@@ -43,9 +43,7 @@ class ModuleAPIEndpoint(BaseAPIView):
"""
model = Module
permission_classes = [
ProjectEntityPermission,
]
permission_classes = [ProjectEntityPermission]
serializer_class = ModuleSerializer
webhook_event = "module"
@@ -60,9 +58,7 @@ class ModuleAPIEndpoint(BaseAPIView):
.prefetch_related(
Prefetch(
"link_module",
queryset=ModuleLink.objects.select_related(
"module", "created_by"
),
queryset=ModuleLink.objects.select_related("module", "created_by"),
)
)
.annotate(
@@ -71,9 +67,10 @@ class ModuleAPIEndpoint(BaseAPIView):
filter=Q(
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
issue_module__deleted_at__isnull=True,
),
distinct=True,
),
)
)
.annotate(
completed_issues=Count(
@@ -82,6 +79,7 @@ class ModuleAPIEndpoint(BaseAPIView):
issue_module__issue__state__group="completed",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
issue_module__deleted_at__isnull=True,
),
distinct=True,
)
@@ -93,6 +91,7 @@ class ModuleAPIEndpoint(BaseAPIView):
issue_module__issue__state__group="cancelled",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
issue_module__deleted_at__isnull=True,
),
distinct=True,
)
@@ -104,6 +103,7 @@ class ModuleAPIEndpoint(BaseAPIView):
issue_module__issue__state__group="started",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
issue_module__deleted_at__isnull=True,
),
distinct=True,
)
@@ -115,6 +115,7 @@ class ModuleAPIEndpoint(BaseAPIView):
issue_module__issue__state__group="unstarted",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
issue_module__deleted_at__isnull=True,
),
distinct=True,
)
@@ -126,6 +127,7 @@ class ModuleAPIEndpoint(BaseAPIView):
issue_module__issue__state__group="backlog",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
issue_module__deleted_at__isnull=True,
),
distinct=True,
)
@@ -137,10 +139,7 @@ class ModuleAPIEndpoint(BaseAPIView):
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,
},
context={"project_id": project_id, "workspace_id": project.workspace_id},
)
if serializer.is_valid():
if (
@@ -183,9 +182,7 @@ class ModuleAPIEndpoint(BaseAPIView):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def patch(self, request, slug, project_id, pk):
module = Module.objects.get(
pk=pk, project_id=project_id, workspace__slug=slug
)
module = Module.objects.get(pk=pk, project_id=project_id, workspace__slug=slug)
current_instance = json.dumps(
ModuleSerializer(module).data, cls=DjangoJSONEncoder
@@ -197,10 +194,7 @@ class ModuleAPIEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST,
)
serializer = ModuleSerializer(
module,
data=request.data,
context={"project_id": project_id},
partial=True,
module, data=request.data, context={"project_id": project_id}, partial=True
)
if serializer.is_valid():
if (
@@ -240,33 +234,21 @@ class ModuleAPIEndpoint(BaseAPIView):
def get(self, request, slug, project_id, pk=None):
if pk:
queryset = (
self.get_queryset().filter(archived_at__isnull=True).get(pk=pk)
)
queryset = self.get_queryset().filter(archived_at__isnull=True).get(pk=pk)
data = ModuleSerializer(
queryset,
fields=self.fields,
expand=self.expand,
queryset, fields=self.fields, expand=self.expand
).data
return Response(
data,
status=status.HTTP_200_OK,
)
return Response(data, status=status.HTTP_200_OK)
return self.paginate(
request=request,
queryset=(self.get_queryset().filter(archived_at__isnull=True)),
on_results=lambda modules: ModuleSerializer(
modules,
many=True,
fields=self.fields,
expand=self.expand,
modules, many=True, fields=self.fields, expand=self.expand
).data,
)
def delete(self, request, slug, project_id, pk):
module = Module.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
module = Module.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
if module.created_by_id != request.user.id and (
not ProjectMember.objects.filter(
workspace__slug=slug,
@@ -282,9 +264,7 @@ class ModuleAPIEndpoint(BaseAPIView):
)
module_issues = list(
ModuleIssue.objects.filter(module_id=pk).values_list(
"issue", flat=True
)
ModuleIssue.objects.filter(module_id=pk).values_list("issue", flat=True)
)
issue_activity.delay(
type="module.activity.deleted",
@@ -298,24 +278,15 @@ class ModuleAPIEndpoint(BaseAPIView):
actor_id=str(request.user.id),
issue_id=None,
project_id=str(project_id),
current_instance=json.dumps(
{
"module_name": str(module.name),
}
),
current_instance=json.dumps({"module_name": str(module.name)}),
epoch=int(timezone.now().timestamp()),
)
module.delete()
# Delete the module issues
ModuleIssue.objects.filter(
module=pk,
project_id=project_id,
).delete()
ModuleIssue.objects.filter(module=pk, project_id=project_id).delete()
# Delete the user favorite module
UserFavorite.objects.filter(
entity_type="module",
entity_identifier=pk,
project_id=project_id,
entity_type="module", entity_identifier=pk, project_id=project_id
).delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -332,16 +303,12 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
webhook_event = "module_issue"
bulk = True
permission_classes = [
ProjectEntityPermission,
]
permission_classes = [ProjectEntityPermission]
def get_queryset(self):
return (
ModuleIssue.objects.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("issue")
)
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -367,11 +334,11 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
def get(self, request, slug, project_id, module_id):
order_by = request.GET.get("order_by", "created_at")
issues = (
Issue.issue_objects.filter(issue_module__module_id=module_id)
Issue.issue_objects.filter(
issue_module__module_id=module_id, issue_module__deleted_at__isnull=True
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -406,10 +373,7 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
request=request,
queryset=(issues),
on_results=lambda issues: IssueSerializer(
issues,
many=True,
fields=self.fields,
expand=self.expand,
issues, many=True, fields=self.fields, expand=self.expand
).data,
)
@@ -417,8 +381,7 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
issues = request.data.get("issues", [])
if not len(issues):
return Response(
{"error": "Issues are required"},
status=status.HTTP_400_BAD_REQUEST,
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
)
module = Module.objects.get(
workspace__slug=slug, project_id=project_id, pk=module_id
@@ -465,16 +428,10 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
)
ModuleIssue.objects.bulk_create(
record_to_create,
batch_size=10,
ignore_conflicts=True,
record_to_create, batch_size=10, ignore_conflicts=True
)
ModuleIssue.objects.bulk_update(
records_to_update,
["module"],
batch_size=10,
)
ModuleIssue.objects.bulk_update(records_to_update, ["module"], batch_size=10)
# Capture Issue Activity
issue_activity.delay(
@@ -510,10 +467,7 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
issue_activity.delay(
type="module.activity.deleted",
requested_data=json.dumps(
{
"module_id": str(module_id),
"issues": [str(module_issue.issue_id)],
}
{"module_id": str(module_id), "issues": [str(module_issue.issue_id)]}
),
actor_id=str(request.user.id),
issue_id=str(issue_id),
@@ -525,9 +479,7 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
permission_classes = [ProjectEntityPermission]
def get_queryset(self):
return (
@@ -541,9 +493,7 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
.prefetch_related(
Prefetch(
"link_module",
queryset=ModuleLink.objects.select_related(
"module", "created_by"
),
queryset=ModuleLink.objects.select_related("module", "created_by"),
)
)
.annotate(
@@ -552,9 +502,10 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
filter=Q(
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
issue_module__deleted_at__isnull=True,
),
distinct=True,
),
)
)
.annotate(
completed_issues=Count(
@@ -563,6 +514,7 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
issue_module__issue__state__group="completed",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
issue_module__deleted_at__isnull=True,
),
distinct=True,
)
@@ -574,6 +526,7 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
issue_module__issue__state__group="cancelled",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
issue_module__deleted_at__isnull=True,
),
distinct=True,
)
@@ -585,6 +538,7 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
issue_module__issue__state__group="started",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
issue_module__deleted_at__isnull=True,
),
distinct=True,
)
@@ -596,6 +550,7 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
issue_module__issue__state__group="unstarted",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
issue_module__deleted_at__isnull=True,
),
distinct=True,
)
@@ -607,6 +562,7 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
issue_module__issue__state__group="backlog",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
issue_module__deleted_at__isnull=True,
),
distinct=True,
)
@@ -619,22 +575,15 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
request=request,
queryset=(self.get_queryset()),
on_results=lambda modules: ModuleSerializer(
modules,
many=True,
fields=self.fields,
expand=self.expand,
modules, many=True, fields=self.fields, expand=self.expand
).data,
)
def post(self, request, slug, project_id, pk):
module = Module.objects.get(
pk=pk, project_id=project_id, workspace__slug=slug
)
module = Module.objects.get(pk=pk, project_id=project_id, workspace__slug=slug)
if module.status not in ["completed", "cancelled"]:
return Response(
{
"error": "Only completed or cancelled modules can be archived"
},
{"error": "Only completed or cancelled modules can be archived"},
status=status.HTTP_400_BAD_REQUEST,
)
module.archived_at = timezone.now()
@@ -648,9 +597,7 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
return Response(status=status.HTTP_204_NO_CONTENT)
def delete(self, request, slug, project_id, pk):
module = Module.objects.get(
pk=pk, project_id=project_id, workspace__slug=slug
)
module = Module.objects.get(pk=pk, project_id=project_id, workspace__slug=slug)
module.archived_at = None
module.save()
return Response(status=status.HTTP_204_NO_CONTENT)
+32 -71
View File
@@ -18,7 +18,7 @@ from plane.app.permissions import ProjectBasePermission
# Module imports
from plane.db.models import (
Cycle,
Inbox,
Intake,
IssueUserProperty,
Module,
Project,
@@ -39,9 +39,7 @@ class ProjectAPIEndpoint(BaseAPIView):
model = Project
webhook_event = "project"
permission_classes = [
ProjectBasePermission,
]
permission_classes = [ProjectBasePermission]
def get_queryset(self):
return (
@@ -54,10 +52,7 @@ class ProjectAPIEndpoint(BaseAPIView):
| Q(network=2)
)
.select_related(
"workspace",
"workspace__owner",
"default_assignee",
"project_lead",
"workspace", "workspace__owner", "default_assignee", "project_lead"
)
.annotate(
is_member=Exists(
@@ -71,9 +66,7 @@ class ProjectAPIEndpoint(BaseAPIView):
)
.annotate(
total_members=ProjectMember.objects.filter(
project_id=OuterRef("id"),
member__is_bot=False,
is_active=True,
project_id=OuterRef("id"), member__is_bot=False, is_active=True
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
@@ -125,8 +118,7 @@ class ProjectAPIEndpoint(BaseAPIView):
Prefetch(
"project_projectmember",
queryset=ProjectMember.objects.filter(
workspace__slug=slug,
is_active=True,
workspace__slug=slug, is_active=True
).select_related("member"),
)
)
@@ -136,18 +128,11 @@ class ProjectAPIEndpoint(BaseAPIView):
request=request,
queryset=(projects),
on_results=lambda projects: ProjectSerializer(
projects,
many=True,
fields=self.fields,
expand=self.expand,
projects, many=True, fields=self.fields, expand=self.expand
).data,
)
project = self.get_queryset().get(workspace__slug=slug, pk=pk)
serializer = ProjectSerializer(
project,
fields=self.fields,
expand=self.expand,
)
serializer = ProjectSerializer(project, fields=self.fields, expand=self.expand)
return Response(serializer.data, status=status.HTTP_200_OK)
def post(self, request, slug):
@@ -161,14 +146,11 @@ class ProjectAPIEndpoint(BaseAPIView):
# Add the user as Administrator to the project
_ = ProjectMember.objects.create(
project_id=serializer.data["id"],
member=request.user,
role=20,
project_id=serializer.data["id"], member=request.user, role=20
)
# Also create the issue property for the user
_ = IssueUserProperty.objects.create(
project_id=serializer.data["id"],
user=request.user,
project_id=serializer.data["id"], user=request.user
)
if serializer.data["project_lead"] is not None and str(
@@ -236,11 +218,7 @@ class ProjectAPIEndpoint(BaseAPIView):
]
)
project = (
self.get_queryset()
.filter(pk=serializer.data["id"])
.first()
)
project = self.get_queryset().filter(pk=serializer.data["id"]).first()
# Model activity
model_activity.delay(
@@ -254,13 +232,8 @@ class ProjectAPIEndpoint(BaseAPIView):
)
serializer = ProjectSerializer(project)
return Response(
serializer.data, status=status.HTTP_201_CREATED
)
return Response(
serializer.errors,
status=status.HTTP_400_BAD_REQUEST,
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError as e:
if "already exists" in str(e):
return Response(
@@ -269,8 +242,7 @@ class ProjectAPIEndpoint(BaseAPIView):
)
except Workspace.DoesNotExist:
return Response(
{"error": "Workspace does not exist"},
status=status.HTTP_404_NOT_FOUND,
{"error": "Workspace does not exist"}, status=status.HTTP_404_NOT_FOUND
)
except ValidationError:
return Response(
@@ -285,6 +257,11 @@ class ProjectAPIEndpoint(BaseAPIView):
current_instance = json.dumps(
ProjectSerializer(project).data, cls=DjangoJSONEncoder
)
intake_view = request.data.get(
"inbox_view", request.data.get("intake_view", False)
)
if project.archived_at:
return Response(
{"error": "Archived project cannot be updated"},
@@ -293,21 +270,20 @@ class ProjectAPIEndpoint(BaseAPIView):
serializer = ProjectSerializer(
project,
data={**request.data},
data={**request.data, "intake_view": intake_view},
context={"workspace_id": workspace.id},
partial=True,
)
if serializer.is_valid():
serializer.save()
if serializer.data["inbox_view"]:
inbox = Inbox.objects.filter(
project=project,
is_default=True,
if serializer.data["intake_view"]:
intake = Intake.objects.filter(
project=project, is_default=True
).first()
if not inbox:
Inbox.objects.create(
name=f"{project.name} Inbox",
if not intake:
Intake.objects.create(
name=f"{project.name} Intake",
project=project,
is_default=True,
)
@@ -316,17 +292,13 @@ class ProjectAPIEndpoint(BaseAPIView):
State.objects.get_or_create(
name="Triage",
group="triage",
description="Default state for managing all Inbox Issues",
description="Default state for managing all Intake Issues",
project_id=pk,
color="#ff7700",
is_triage=True,
)
project = (
self.get_queryset()
.filter(pk=serializer.data["id"])
.first()
)
project = self.get_queryset().filter(pk=serializer.data["id"]).first()
model_activity.delay(
model_name="project",
@@ -340,9 +312,7 @@ class ProjectAPIEndpoint(BaseAPIView):
serializer = ProjectSerializer(project)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError as e:
if "already exists" in str(e):
return Response(
@@ -351,8 +321,7 @@ class ProjectAPIEndpoint(BaseAPIView):
)
except (Project.DoesNotExist, Workspace.DoesNotExist):
return Response(
{"error": "Project does not exist"},
status=status.HTTP_404_NOT_FOUND,
{"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND
)
except ValidationError:
return Response(
@@ -364,28 +333,20 @@ class ProjectAPIEndpoint(BaseAPIView):
project = Project.objects.get(pk=pk, workspace__slug=slug)
# Delete the user favorite cycle
UserFavorite.objects.filter(
entity_type="project",
entity_identifier=pk,
project_id=pk,
entity_type="project", entity_identifier=pk, project_id=pk
).delete()
project.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class ProjectArchiveUnarchiveAPIEndpoint(BaseAPIView):
permission_classes = [
ProjectBasePermission,
]
permission_classes = [ProjectBasePermission]
def post(self, request, slug, project_id):
project = Project.objects.get(pk=project_id, workspace__slug=slug)
project.archived_at = timezone.now()
project.save()
UserFavorite.objects.filter(
workspace__slug=slug,
project=project_id,
).delete()
UserFavorite.objects.filter(workspace__slug=slug, project=project_id).delete()
return Response(status=status.HTTP_204_NO_CONTENT)
def delete(self, request, slug, project_id):
+5 -17
View File
@@ -16,9 +16,7 @@ from .base import BaseAPIView
class StateAPIEndpoint(BaseAPIView):
serializer_class = StateSerializer
model = State
permission_classes = [
ProjectEntityPermission,
]
permission_classes = [ProjectEntityPermission]
def get_queryset(self):
return (
@@ -67,9 +65,7 @@ class StateAPIEndpoint(BaseAPIView):
serializer.save(project_id=project_id)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError:
state = State.objects.filter(
workspace__slug=slug,
@@ -96,19 +92,13 @@ class StateAPIEndpoint(BaseAPIView):
request=request,
queryset=(self.get_queryset()),
on_results=lambda states: StateSerializer(
states,
many=True,
fields=self.fields,
expand=self.expand,
states, many=True, fields=self.fields, expand=self.expand
).data,
)
def delete(self, request, slug, project_id, state_id):
state = State.objects.get(
is_triage=False,
pk=state_id,
project_id=project_id,
workspace__slug=slug,
is_triage=False, pk=state_id, project_id=project_id, workspace__slug=slug
)
if state.default:
@@ -122,9 +112,7 @@ class StateAPIEndpoint(BaseAPIView):
if issue_exist:
return Response(
{
"error": "The state is not empty, only empty states can be deleted"
},
{"error": "The state is not empty, only empty states can be deleted"},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -25,10 +25,7 @@ class APIKeyAuthentication(authentication.BaseAuthentication):
def validate_api_token(self, token):
try:
api_token = APIToken.objects.get(
Q(
Q(expired_at__gt=timezone.now())
| Q(expired_at__isnull=True)
),
Q(Q(expired_at__gt=timezone.now()) | Q(expired_at__isnull=True)),
token=token,
is_active=True,
)
+1 -1
View File
@@ -12,4 +12,4 @@ from .project import (
ProjectMemberPermission,
ProjectLitePermission,
)
from .base import allow_permission, ROLE
from .base import allow_permission, ROLE
+2 -3
View File
@@ -5,6 +5,7 @@ from rest_framework import status
from enum import Enum
class ROLE(Enum):
ADMIN = 20
MEMBER = 15
@@ -15,7 +16,6 @@ def allow_permission(allowed_roles, level="PROJECT", creator=False, model=None):
def decorator(view_func):
@wraps(view_func)
def _wrapped_view(instance, request, *args, **kwargs):
# Check for creator if required
if creator and model:
obj = model.objects.filter(
@@ -26,8 +26,7 @@ def allow_permission(allowed_roles, level="PROJECT", creator=False, model=None):
# Convert allowed_roles to their values if they are enum members
allowed_role_values = [
role.value if isinstance(role, ROLE) else role
for role in allowed_roles
role.value if isinstance(role, ROLE) else role for role in allowed_roles
]
# Check role permissions
+2 -6
View File
@@ -18,9 +18,7 @@ class ProjectBasePermission(BasePermission):
## Safe Methods -> Handle the filtering logic in queryset
if request.method in SAFE_METHODS:
return WorkspaceMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
is_active=True,
workspace__slug=view.workspace_slug, member=request.user, is_active=True
).exists()
## Only workspace owners or admins can create the projects
@@ -50,9 +48,7 @@ class ProjectMemberPermission(BasePermission):
## Safe Methods -> Handle the filtering logic in queryset
if request.method in SAFE_METHODS:
return ProjectMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
is_active=True,
workspace__slug=view.workspace_slug, member=request.user, is_active=True
).exists()
## Only workspace owners or admins can create the projects
if request.method == "POST":
+4 -12
View File
@@ -50,9 +50,7 @@ class WorkspaceOwnerPermission(BasePermission):
return False
return WorkspaceMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
role=Admin,
workspace__slug=view.workspace_slug, member=request.user, role=Admin
).exists()
@@ -77,9 +75,7 @@ class WorkspaceEntityPermission(BasePermission):
## Safe Methods -> Handle the filtering logic in queryset
if request.method in SAFE_METHODS:
return WorkspaceMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
is_active=True,
workspace__slug=view.workspace_slug, member=request.user, is_active=True
).exists()
return WorkspaceMember.objects.filter(
@@ -96,9 +92,7 @@ class WorkspaceViewerPermission(BasePermission):
return False
return WorkspaceMember.objects.filter(
member=request.user,
workspace__slug=view.workspace_slug,
is_active=True,
member=request.user, workspace__slug=view.workspace_slug, is_active=True
).exists()
@@ -108,7 +102,5 @@ class WorkspaceUserPermission(BasePermission):
return False
return WorkspaceMember.objects.filter(
member=request.user,
workspace__slug=view.workspace_slug,
is_active=True,
member=request.user, workspace__slug=view.workspace_slug, is_active=True
).exists()
+9 -14
View File
@@ -36,9 +36,7 @@ from .project import (
ProjectMemberRoleSerializer,
)
from .state import StateSerializer, StateLiteSerializer
from .view import (
IssueViewSerializer,
)
from .view import IssueViewSerializer
from .cycle import (
CycleSerializer,
CycleIssueSerializer,
@@ -57,7 +55,7 @@ from .issue import (
IssueFlatSerializer,
IssueStateSerializer,
IssueLinkSerializer,
IssueInboxSerializer,
IssueIntakeSerializer,
IssueLiteSerializer,
IssueAttachmentSerializer,
IssueSubscriberSerializer,
@@ -102,20 +100,17 @@ from .estimate import (
WorkspaceEstimateSerializer,
)
from .inbox import (
InboxSerializer,
InboxIssueSerializer,
IssueStateInboxSerializer,
InboxIssueLiteSerializer,
InboxIssueDetailSerializer,
from .intake import (
IntakeSerializer,
IntakeIssueSerializer,
IssueStateIntakeSerializer,
IntakeIssueLiteSerializer,
IntakeIssueDetailSerializer,
)
from .analytic import AnalyticViewSerializer
from .notification import (
NotificationSerializer,
UserNotificationPreferenceSerializer,
)
from .notification import NotificationSerializer, UserNotificationPreferenceSerializer
from .exporter import ExporterHistorySerializer
+1 -4
View File
@@ -7,10 +7,7 @@ class AnalyticViewSerializer(BaseSerializer):
class Meta:
model = AnalyticView
fields = "__all__"
read_only_fields = [
"workspace",
"query",
]
read_only_fields = ["workspace", "query"]
def create(self, validated_data):
query_params = validated_data.get("query_dict", {})
+1 -6
View File
@@ -6,9 +6,4 @@ class FileAssetSerializer(BaseSerializer):
class Meta:
model = FileAsset
fields = "__all__"
read_only_fields = [
"created_by",
"updated_by",
"created_at",
"updated_at",
]
read_only_fields = ["created_by", "updated_by", "created_at", "updated_at"]
+51 -52
View File
@@ -49,44 +49,47 @@ class DynamicBaseSerializer(BaseSerializer):
allowed.append(list(item.keys())[0])
for field in allowed:
from . import (
WorkspaceLiteSerializer,
ProjectLiteSerializer,
UserLiteSerializer,
StateLiteSerializer,
IssueSerializer,
LabelSerializer,
CycleIssueSerializer,
IssueLiteSerializer,
IssueRelationSerializer,
InboxIssueLiteSerializer,
IssueReactionLiteSerializer,
IssueLinkLiteSerializer,
)
if field not in self.fields:
from . import (
WorkspaceLiteSerializer,
ProjectLiteSerializer,
UserLiteSerializer,
StateLiteSerializer,
IssueSerializer,
LabelSerializer,
CycleIssueSerializer,
IssueLiteSerializer,
IssueRelationSerializer,
IntakeIssueLiteSerializer,
IssueReactionLiteSerializer,
IssueLinkLiteSerializer,
RelatedIssueSerializer,
)
# 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": IssueLiteSerializer,
"issue_relation": IssueRelationSerializer,
"issue_inbox": InboxIssueLiteSerializer,
"issue_reactions": IssueReactionLiteSerializer,
"issue_link": IssueLinkLiteSerializer,
"sub_issues": IssueLiteSerializer,
}
# 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": IssueLiteSerializer,
"issue_relation": IssueRelationSerializer,
"issue_intake": IntakeIssueLiteSerializer,
"issue_related": RelatedIssueSerializer,
"issue_reactions": IssueReactionLiteSerializer,
"issue_link": IssueLinkLiteSerializer,
"sub_issues": IssueLiteSerializer,
}
if field not in self.fields and field in expansion:
self.fields[field] = expansion[field](
@@ -99,11 +102,12 @@ class DynamicBaseSerializer(BaseSerializer):
"labels",
"issue_cycle",
"issue_relation",
"issue_inbox",
"issue_intake",
"issue_reactions",
"issue_attachment",
"issue_link",
"sub_issues",
"issue_related",
]
else False
)
@@ -128,11 +132,12 @@ class DynamicBaseSerializer(BaseSerializer):
LabelSerializer,
CycleIssueSerializer,
IssueRelationSerializer,
InboxIssueLiteSerializer,
IntakeIssueLiteSerializer,
IssueLiteSerializer,
IssueReactionLiteSerializer,
IssueAttachmentLiteSerializer,
IssueLinkLiteSerializer,
RelatedIssueSerializer,
)
# Expansion mapper
@@ -153,7 +158,8 @@ class DynamicBaseSerializer(BaseSerializer):
"issue_cycle": CycleIssueSerializer,
"parent": IssueLiteSerializer,
"issue_relation": IssueRelationSerializer,
"issue_inbox": InboxIssueLiteSerializer,
"issue_intake": IntakeIssueLiteSerializer,
"issue_related": RelatedIssueSerializer,
"issue_reactions": IssueReactionLiteSerializer,
"issue_attachment": IssueAttachmentLiteSerializer,
"issue_link": IssueLinkLiteSerializer,
@@ -172,15 +178,10 @@ class DynamicBaseSerializer(BaseSerializer):
response[expand] = exp_serializer.data
else:
# You might need to handle this case differently
response[expand] = getattr(
instance, f"{expand}_id", None
)
response[expand] = getattr(instance, f"{expand}_id", None)
# Check if issue_attachments is in fields or expand
if (
"issue_attachments" in self.fields
or "issue_attachments" in self.expand
):
if "issue_attachments" in self.fields or "issue_attachments" in self.expand:
# Import the model here to avoid circular imports
from plane.db.models import FileAsset
@@ -193,11 +194,9 @@ class DynamicBaseSerializer(BaseSerializer):
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
)
# Serialize issue_attachments and add them to the response
response["issue_attachments"] = (
IssueAttachmentLiteSerializer(
issue_attachments, many=True
).data
)
response["issue_attachments"] = IssueAttachmentLiteSerializer(
issue_attachments, many=True
).data
else:
response["issue_attachments"] = []
+6 -24
View File
@@ -4,11 +4,7 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .issue import IssueStateSerializer
from plane.db.models import (
Cycle,
CycleIssue,
CycleUserProperties,
)
from plane.db.models import Cycle, CycleIssue, CycleUserProperties
class CycleWriteSerializer(BaseSerializer):
@@ -18,20 +14,13 @@ class CycleWriteSerializer(BaseSerializer):
and data.get("end_date", None) is not None
and data.get("start_date", None) > data.get("end_date", None)
):
raise serializers.ValidationError(
"Start date cannot exceed end date"
)
raise serializers.ValidationError("Start date cannot exceed end date")
return data
class Meta:
model = Cycle
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"owned_by",
"archived_at",
]
read_only_fields = ["workspace", "project", "owned_by", "archived_at"]
class CycleSerializer(BaseSerializer):
@@ -87,18 +76,11 @@ class CycleIssueSerializer(BaseSerializer):
class Meta:
model = CycleIssue
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"cycle",
]
read_only_fields = ["workspace", "project", "cycle"]
class CycleUserPropertiesSerializer(BaseSerializer):
class Meta:
model = CycleUserProperties
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"cycle" "user",
]
read_only_fields = ["workspace", "project", "cycle" "user"]
+11 -36
View File
@@ -22,16 +22,10 @@ from plane.db.models import (
class DraftIssueCreateSerializer(BaseSerializer):
# ids
state_id = serializers.PrimaryKeyRelatedField(
source="state",
queryset=State.objects.all(),
required=False,
allow_null=True,
source="state", queryset=State.objects.all(), required=False, allow_null=True
)
parent_id = serializers.PrimaryKeyRelatedField(
source="parent",
queryset=Issue.objects.all(),
required=False,
allow_null=True,
source="parent", queryset=Issue.objects.all(), required=False, allow_null=True
)
label_ids = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
@@ -69,9 +63,7 @@ class DraftIssueCreateSerializer(BaseSerializer):
and data.get("target_date", None) is not None
and data.get("start_date", None) > data.get("target_date", None)
):
raise serializers.ValidationError(
"Start date cannot exceed target date"
)
raise serializers.ValidationError("Start date cannot exceed target date")
return data
def create(self, validated_data):
@@ -86,9 +78,7 @@ class DraftIssueCreateSerializer(BaseSerializer):
# Create Issue
issue = DraftIssue.objects.create(
**validated_data,
workspace_id=workspace_id,
project_id=project_id,
**validated_data, workspace_id=workspace_id, project_id=project_id
)
# Issue Audit Users
@@ -169,9 +159,7 @@ class DraftIssueCreateSerializer(BaseSerializer):
updated_by_id = instance.updated_by_id
if assignees is not None:
DraftIssueAssignee.objects.filter(draft_issue=instance).delete(
soft=False
)
DraftIssueAssignee.objects.filter(draft_issue=instance).delete()
DraftIssueAssignee.objects.bulk_create(
[
DraftIssueAssignee(
@@ -188,9 +176,7 @@ class DraftIssueCreateSerializer(BaseSerializer):
)
if labels is not None:
DraftIssueLabel.objects.filter(draft_issue=instance).delete(
soft=False
)
DraftIssueLabel.objects.filter(draft_issue=instance).delete()
DraftIssueLabel.objects.bulk_create(
[
DraftIssueLabel(
@@ -208,7 +194,7 @@ class DraftIssueCreateSerializer(BaseSerializer):
if cycle_id != "not_provided":
DraftIssueCycle.objects.filter(draft_issue=instance).delete()
if cycle_id is not None:
if cycle_id:
DraftIssueCycle.objects.create(
cycle_id=cycle_id,
draft_issue=instance,
@@ -243,20 +229,11 @@ class DraftIssueCreateSerializer(BaseSerializer):
class DraftIssueSerializer(BaseSerializer):
# ids
cycle_id = serializers.PrimaryKeyRelatedField(read_only=True)
module_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
module_ids = serializers.ListField(child=serializers.UUIDField(), required=False)
# Many to many
label_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
assignee_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
label_ids = serializers.ListField(child=serializers.UUIDField(), required=False)
assignee_ids = serializers.ListField(child=serializers.UUIDField(), required=False)
class Meta:
model = DraftIssue
@@ -290,7 +267,5 @@ class DraftIssueDetailSerializer(DraftIssueSerializer):
description_html = serializers.CharField()
class Meta(DraftIssueSerializer.Meta):
fields = DraftIssueSerializer.Meta.fields + [
"description_html",
]
fields = DraftIssueSerializer.Meta.fields + ["description_html"]
read_only_fields = fields
+5 -23
View File
@@ -7,14 +7,10 @@ from rest_framework import serializers
class EstimateSerializer(BaseSerializer):
class Meta:
model = Estimate
fields = "__all__"
read_only_fields = [
"workspace",
"project",
]
read_only_fields = ["workspace", "project"]
class EstimatePointSerializer(BaseSerializer):
@@ -23,19 +19,13 @@ class EstimatePointSerializer(BaseSerializer):
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"
)
raise serializers.ValidationError("Value can't be more than 20 characters")
return data
class Meta:
model = EstimatePoint
fields = "__all__"
read_only_fields = [
"estimate",
"workspace",
"project",
]
read_only_fields = ["estimate", "workspace", "project"]
class EstimateReadSerializer(BaseSerializer):
@@ -44,11 +34,7 @@ class EstimateReadSerializer(BaseSerializer):
class Meta:
model = Estimate
fields = "__all__"
read_only_fields = [
"points",
"name",
"description",
]
read_only_fields = ["points", "name", "description"]
class WorkspaceEstimateSerializer(BaseSerializer):
@@ -57,8 +43,4 @@ class WorkspaceEstimateSerializer(BaseSerializer):
class Meta:
model = Estimate
fields = "__all__"
read_only_fields = [
"points",
"name",
"description",
]
read_only_fields = ["points", "name", "description"]
+1 -3
View File
@@ -5,9 +5,7 @@ from .user import UserLiteSerializer
class ExporterHistorySerializer(BaseSerializer):
initiated_by_detail = UserLiteSerializer(
source="initiated_by", read_only=True
)
initiated_by_detail = UserLiteSerializer(source="initiated_by", read_only=True)
class Meta:
model = ExporterHistory
+2 -16
View File
@@ -1,18 +1,9 @@
from rest_framework import serializers
from plane.db.models import (
UserFavorite,
Cycle,
Module,
Issue,
IssueView,
Page,
Project,
)
from plane.db.models import UserFavorite, Cycle, Module, Issue, IssueView, Page, Project
class ProjectFavoriteLiteSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = ["id", "name", "logo_props"]
@@ -33,21 +24,18 @@ class PageFavoriteLiteSerializer(serializers.ModelSerializer):
class CycleFavoriteLiteSerializer(serializers.ModelSerializer):
class Meta:
model = Cycle
fields = ["id", "name", "logo_props", "project_id"]
class ModuleFavoriteLiteSerializer(serializers.ModelSerializer):
class Meta:
model = Module
fields = ["id", "name", "logo_props", "project_id"]
class ViewFavoriteSerializer(serializers.ModelSerializer):
class Meta:
model = IssueView
fields = ["id", "name", "logo_props", "project_id"]
@@ -89,9 +77,7 @@ class UserFavoriteSerializer(serializers.ModelSerializer):
entity_type = obj.entity_type
entity_identifier = obj.entity_identifier
entity_model, entity_serializer = get_entity_model_and_serializer(
entity_type
)
entity_model, entity_serializer = get_entity_model_and_serializer(entity_type)
if entity_model and entity_serializer:
try:
entity = entity_model.objects.get(pk=entity_identifier)
+2 -6
View File
@@ -7,13 +7,9 @@ from plane.db.models import Importer
class ImporterSerializer(BaseSerializer):
initiated_by_detail = UserLiteSerializer(
source="initiated_by", read_only=True
)
initiated_by_detail = UserLiteSerializer(source="initiated_by", read_only=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer(
source="workspace", read_only=True
)
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
class Meta:
model = Importer
@@ -3,35 +3,28 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .issue import (
IssueInboxSerializer,
LabelLiteSerializer,
IssueDetailSerializer,
)
from .issue import IssueIntakeSerializer, LabelLiteSerializer, IssueDetailSerializer
from .project import ProjectLiteSerializer
from .state import StateLiteSerializer
from .user import UserLiteSerializer
from plane.db.models import Inbox, InboxIssue, Issue
from plane.db.models import Intake, IntakeIssue, Issue
class InboxSerializer(BaseSerializer):
class IntakeSerializer(BaseSerializer):
project_detail = ProjectLiteSerializer(source="project", read_only=True)
pending_issue_count = serializers.IntegerField(read_only=True)
class Meta:
model = Inbox
model = Intake
fields = "__all__"
read_only_fields = [
"project",
"workspace",
]
read_only_fields = ["project", "workspace"]
class InboxIssueSerializer(BaseSerializer):
issue = IssueInboxSerializer(read_only=True)
class IntakeIssueSerializer(BaseSerializer):
issue = IssueIntakeSerializer(read_only=True)
class Meta:
model = InboxIssue
model = IntakeIssue
fields = [
"id",
"status",
@@ -41,10 +34,7 @@ class InboxIssueSerializer(BaseSerializer):
"issue",
"created_by",
]
read_only_fields = [
"project",
"workspace",
]
read_only_fields = ["project", "workspace"]
def to_representation(self, instance):
# Pass the annotated fields to the Issue instance if they exist
@@ -53,14 +43,14 @@ class InboxIssueSerializer(BaseSerializer):
return super().to_representation(instance)
class InboxIssueDetailSerializer(BaseSerializer):
class IntakeIssueDetailSerializer(BaseSerializer):
issue = IssueDetailSerializer(read_only=True)
duplicate_issue_detail = IssueInboxSerializer(
duplicate_issue_detail = IssueIntakeSerializer(
read_only=True, source="duplicate_to"
)
class Meta:
model = InboxIssue
model = IntakeIssue
fields = [
"id",
"status",
@@ -70,10 +60,7 @@ class InboxIssueDetailSerializer(BaseSerializer):
"source",
"issue",
]
read_only_fields = [
"project",
"workspace",
]
read_only_fields = ["project", "workspace"]
def to_representation(self, instance):
# Pass the annotated fields to the Issue instance if they exist
@@ -85,24 +72,20 @@ class InboxIssueDetailSerializer(BaseSerializer):
return super().to_representation(instance)
class InboxIssueLiteSerializer(BaseSerializer):
class IntakeIssueLiteSerializer(BaseSerializer):
class Meta:
model = InboxIssue
model = IntakeIssue
fields = ["id", "status", "duplicate_to", "snoozed_till", "source"]
read_only_fields = fields
class IssueStateInboxSerializer(BaseSerializer):
class IssueStateIntakeSerializer(BaseSerializer):
state_detail = StateLiteSerializer(read_only=True, source="state")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
label_details = LabelLiteSerializer(
read_only=True, source="labels", many=True
)
assignee_details = UserLiteSerializer(
read_only=True, source="assignees", many=True
)
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)
issue_inbox = InboxIssueLiteSerializer(read_only=True, many=True)
issue_intake = IntakeIssueLiteSerializer(read_only=True, many=True)
class Meta:
model = Issue
+37 -149
View File
@@ -60,12 +60,7 @@ class IssueProjectLiteSerializer(BaseSerializer):
class Meta:
model = Issue
fields = [
"id",
"project_detail",
"name",
"sequence_id",
]
fields = ["id", "project_detail", "name", "sequence_id"]
read_only_fields = fields
@@ -74,16 +69,10 @@ class IssueProjectLiteSerializer(BaseSerializer):
class IssueCreateSerializer(BaseSerializer):
# ids
state_id = serializers.PrimaryKeyRelatedField(
source="state",
queryset=State.objects.all(),
required=False,
allow_null=True,
source="state", queryset=State.objects.all(), required=False, allow_null=True
)
parent_id = serializers.PrimaryKeyRelatedField(
source="parent",
queryset=Issue.objects.all(),
required=False,
allow_null=True,
source="parent", queryset=Issue.objects.all(), required=False, allow_null=True
)
label_ids = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
@@ -95,6 +84,8 @@ class IssueCreateSerializer(BaseSerializer):
write_only=True,
required=False,
)
project_id = serializers.UUIDField(source="project.id", read_only=True)
workspace_id = serializers.UUIDField(source="workspace.id", read_only=True)
class Meta:
model = Issue
@@ -122,9 +113,7 @@ class IssueCreateSerializer(BaseSerializer):
and data.get("target_date", None) is not None
and data.get("start_date", None) > data.get("target_date", None)
):
raise serializers.ValidationError(
"Start date cannot exceed target date"
)
raise serializers.ValidationError("Start date cannot exceed target date")
return data
def create(self, validated_data):
@@ -136,10 +125,7 @@ class IssueCreateSerializer(BaseSerializer):
default_assignee_id = self.context["default_assignee_id"]
# Create Issue
issue = Issue.objects.create(
**validated_data,
project_id=project_id,
)
issue = Issue.objects.create(**validated_data, project_id=project_id)
# Issue Audit Users
created_by_id = issue.created_by_id
@@ -201,7 +187,7 @@ class IssueCreateSerializer(BaseSerializer):
updated_by_id = instance.updated_by_id
if assignees is not None:
IssueAssignee.objects.filter(issue=instance).delete(soft=False)
IssueAssignee.objects.filter(issue=instance).delete()
IssueAssignee.objects.bulk_create(
[
IssueAssignee(
@@ -218,7 +204,7 @@ class IssueCreateSerializer(BaseSerializer):
)
if labels is not None:
IssueLabel.objects.filter(issue=instance).delete(soft=False)
IssueLabel.objects.filter(issue=instance).delete()
IssueLabel.objects.bulk_create(
[
IssueLabel(
@@ -243,9 +229,7 @@ class IssueActivitySerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor")
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(
read_only=True, source="workspace"
)
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
class Meta:
model = IssueActivity
@@ -256,11 +240,7 @@ class IssueUserPropertySerializer(BaseSerializer):
class Meta:
model = IssueUserProperty
fields = "__all__"
read_only_fields = [
"user",
"workspace",
"project",
]
read_only_fields = ["user", "workspace", "project"]
class LabelSerializer(BaseSerializer):
@@ -275,30 +255,20 @@ class LabelSerializer(BaseSerializer):
"workspace_id",
"sort_order",
]
read_only_fields = [
"workspace",
"project",
]
read_only_fields = ["workspace", "project"]
class LabelLiteSerializer(BaseSerializer):
class Meta:
model = Label
fields = [
"id",
"name",
"color",
]
fields = ["id", "name", "color"]
class IssueLabelSerializer(BaseSerializer):
class Meta:
model = IssueLabel
fields = "__all__"
read_only_fields = [
"workspace",
"project",
]
read_only_fields = ["workspace", "project"]
class IssueRelationSerializer(BaseSerializer):
@@ -314,17 +284,8 @@ class IssueRelationSerializer(BaseSerializer):
class Meta:
model = IssueRelation
fields = [
"id",
"project_id",
"sequence_id",
"relation_type",
"name",
]
read_only_fields = [
"workspace",
"project",
]
fields = ["id", "project_id", "sequence_id", "relation_type", "name"]
read_only_fields = ["workspace", "project"]
class RelatedIssueSerializer(BaseSerializer):
@@ -332,25 +293,14 @@ class RelatedIssueSerializer(BaseSerializer):
project_id = serializers.PrimaryKeyRelatedField(
source="issue.project_id", read_only=True
)
sequence_id = serializers.IntegerField(
source="issue.sequence_id", read_only=True
)
sequence_id = serializers.IntegerField(source="issue.sequence_id", read_only=True)
name = serializers.CharField(source="issue.name", read_only=True)
relation_type = serializers.CharField(read_only=True)
class Meta:
model = IssueRelation
fields = [
"id",
"project_id",
"sequence_id",
"relation_type",
"name",
]
read_only_fields = [
"workspace",
"project",
]
fields = ["id", "project_id", "sequence_id", "relation_type", "name"]
read_only_fields = ["workspace", "project"]
class IssueAssigneeSerializer(BaseSerializer):
@@ -458,8 +408,7 @@ class IssueLinkSerializer(BaseSerializer):
# Validation if url already exists
def create(self, validated_data):
if IssueLink.objects.filter(
url=validated_data.get("url"),
issue_id=validated_data.get("issue_id"),
url=validated_data.get("url"), issue_id=validated_data.get("issue_id")
).exists():
raise serializers.ValidationError(
{"error": "URL already exists for this Issue"}
@@ -469,8 +418,7 @@ class IssueLinkSerializer(BaseSerializer):
def update(self, instance, validated_data):
if (
IssueLink.objects.filter(
url=validated_data.get("url"),
issue_id=instance.issue_id,
url=validated_data.get("url"), issue_id=instance.issue_id
)
.exclude(pk=instance.id)
.exists()
@@ -498,7 +446,6 @@ class IssueLinkLiteSerializer(BaseSerializer):
class IssueAttachmentSerializer(BaseSerializer):
asset_url = serializers.CharField(read_only=True)
class Meta:
@@ -536,37 +483,20 @@ class IssueReactionSerializer(BaseSerializer):
class Meta:
model = IssueReaction
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"issue",
"actor",
"deleted_at",
]
read_only_fields = ["workspace", "project", "issue", "actor", "deleted_at"]
class IssueReactionLiteSerializer(DynamicBaseSerializer):
class Meta:
model = IssueReaction
fields = [
"id",
"actor",
"issue",
"reaction",
]
fields = ["id", "actor", "issue", "reaction"]
class CommentReactionSerializer(BaseSerializer):
class Meta:
model = CommentReaction
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"comment",
"actor",
"deleted_at",
]
read_only_fields = ["workspace", "project", "comment", "actor", "deleted_at"]
class IssueVoteSerializer(BaseSerializer):
@@ -574,14 +504,7 @@ class IssueVoteSerializer(BaseSerializer):
class Meta:
model = IssueVote
fields = [
"issue",
"vote",
"workspace",
"project",
"actor",
"actor_detail",
]
fields = ["issue", "vote", "workspace", "project", "actor", "actor_detail"]
read_only_fields = fields
@@ -589,9 +512,7 @@ class IssueCommentSerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor")
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(
read_only=True, source="workspace"
)
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
comment_reactions = CommentReactionSerializer(read_only=True, many=True)
is_member = serializers.BooleanField(read_only=True)
@@ -615,25 +536,15 @@ class IssueStateFlatSerializer(BaseSerializer):
class Meta:
model = Issue
fields = [
"id",
"sequence_id",
"name",
"state_detail",
"project_detail",
]
fields = ["id", "sequence_id", "name", "state_detail", "project_detail"]
# Issue Serializer with state details
class IssueStateSerializer(DynamicBaseSerializer):
label_details = LabelLiteSerializer(
read_only=True, source="labels", many=True
)
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
state_detail = StateLiteSerializer(read_only=True, source="state")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
assignee_details = UserLiteSerializer(
read_only=True, source="assignees", many=True
)
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
sub_issues_count = serializers.IntegerField(read_only=True)
attachment_count = serializers.IntegerField(read_only=True)
link_count = serializers.IntegerField(read_only=True)
@@ -643,11 +554,8 @@ class IssueStateSerializer(DynamicBaseSerializer):
fields = "__all__"
class IssueInboxSerializer(DynamicBaseSerializer):
label_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
class IssueIntakeSerializer(DynamicBaseSerializer):
label_ids = serializers.ListField(child=serializers.UUIDField(), required=False)
class Meta:
model = Issue
@@ -667,20 +575,11 @@ class IssueInboxSerializer(DynamicBaseSerializer):
class IssueSerializer(DynamicBaseSerializer):
# ids
cycle_id = serializers.PrimaryKeyRelatedField(read_only=True)
module_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
module_ids = serializers.ListField(child=serializers.UUIDField(), required=False)
# Many to many
label_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
assignee_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
label_ids = serializers.ListField(child=serializers.UUIDField(), required=False)
assignee_ids = serializers.ListField(child=serializers.UUIDField(), required=False)
# Count items
sub_issues_count = serializers.IntegerField(read_only=True)
@@ -722,11 +621,7 @@ class IssueSerializer(DynamicBaseSerializer):
class IssueLiteSerializer(DynamicBaseSerializer):
class Meta:
model = Issue
fields = [
"id",
"sequence_id",
"project_id",
]
fields = ["id", "sequence_id", "project_id"]
read_only_fields = fields
@@ -735,10 +630,7 @@ class IssueDetailSerializer(IssueSerializer):
is_subscribed = serializers.BooleanField(read_only=True)
class Meta(IssueSerializer.Meta):
fields = IssueSerializer.Meta.fields + [
"description_html",
"is_subscribed",
]
fields = IssueSerializer.Meta.fields + ["description_html", "is_subscribed"]
read_only_fields = fields
@@ -774,8 +666,4 @@ class IssueSubscriberSerializer(BaseSerializer):
class Meta:
model = IssueSubscriber
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"issue",
]
read_only_fields = ["workspace", "project", "issue"]
+7 -20
View File
@@ -21,10 +21,7 @@ from plane.db.models import (
class ModuleWriteSerializer(BaseSerializer):
lead_id = serializers.PrimaryKeyRelatedField(
source="lead",
queryset=User.objects.all(),
required=False,
allow_null=True,
source="lead", queryset=User.objects.all(), required=False, allow_null=True
)
member_ids = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
@@ -48,9 +45,7 @@ class ModuleWriteSerializer(BaseSerializer):
def to_representation(self, instance):
data = super().to_representation(instance)
data["member_ids"] = [
str(member.id) for member in instance.members.all()
]
data["member_ids"] = [str(member.id) for member in instance.members.all()]
return data
def validate(self, data):
@@ -59,9 +54,7 @@ class ModuleWriteSerializer(BaseSerializer):
and data.get("target_date", None) is not None
and data.get("start_date", None) > data.get("target_date", None)
):
raise serializers.ValidationError(
"Start date cannot exceed target date"
)
raise serializers.ValidationError("Start date cannot exceed target date")
return data
def create(self, validated_data):
@@ -71,9 +64,7 @@ class ModuleWriteSerializer(BaseSerializer):
module_name = validated_data.get("name")
if module_name:
# Lookup for the module name in the module table for that project
if Module.objects.filter(
name=module_name, project=project
).exists():
if Module.objects.filter(name=module_name, project=project).exists():
raise serializers.ValidationError(
{"error": "Module with this name already exists"}
)
@@ -104,9 +95,7 @@ class ModuleWriteSerializer(BaseSerializer):
if module_name:
# Lookup for the module name in the module table for that project
if (
Module.objects.filter(
name=module_name, project=instance.project
)
Module.objects.filter(name=module_name, project=instance.project)
.exclude(id=instance.id)
.exists()
):
@@ -203,8 +192,7 @@ class ModuleLinkSerializer(BaseSerializer):
def create(self, validated_data):
validated_data["url"] = self.validate_url(validated_data.get("url"))
if ModuleLink.objects.filter(
url=validated_data.get("url"),
module_id=validated_data.get("module_id"),
url=validated_data.get("url"), module_id=validated_data.get("module_id")
).exists():
raise serializers.ValidationError({"error": "URL already exists."})
return super().create(validated_data)
@@ -213,8 +201,7 @@ class ModuleLinkSerializer(BaseSerializer):
validated_data["url"] = self.validate_url(validated_data.get("url"))
if (
ModuleLink.objects.filter(
url=validated_data.get("url"),
module_id=instance.module_id,
url=validated_data.get("url"), module_id=instance.module_id
)
.exclude(pk=instance.id)
.exists()
@@ -8,10 +8,9 @@ from rest_framework import serializers
class NotificationSerializer(BaseSerializer):
triggered_by_details = UserLiteSerializer(
read_only=True, source="triggered_by"
)
triggered_by_details = UserLiteSerializer(read_only=True, source="triggered_by")
is_inbox_issue = serializers.BooleanField(read_only=True)
is_intake_issue = serializers.BooleanField(read_only=True)
is_mentioned_notification = serializers.BooleanField(read_only=True)
class Meta:
+8 -31
View File
@@ -22,14 +22,8 @@ class PageSerializer(BaseSerializer):
required=False,
)
# Many to many
label_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
project_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
label_ids = serializers.ListField(child=serializers.UUIDField(), required=False)
project_ids = serializers.ListField(child=serializers.UUIDField(), required=False)
class Meta:
model = Page
@@ -54,10 +48,7 @@ class PageSerializer(BaseSerializer):
"label_ids",
"project_ids",
]
read_only_fields = [
"workspace",
"owned_by",
]
read_only_fields = ["workspace", "owned_by"]
def create(self, validated_data):
labels = validated_data.pop("labels", None)
@@ -127,9 +118,7 @@ class PageDetailSerializer(PageSerializer):
description_html = serializers.CharField()
class Meta(PageSerializer.Meta):
fields = PageSerializer.Meta.fields + [
"description_html",
]
fields = PageSerializer.Meta.fields + ["description_html"]
class SubPageSerializer(BaseSerializer):
@@ -138,10 +127,7 @@ class SubPageSerializer(BaseSerializer):
class Meta:
model = PageLog
fields = "__all__"
read_only_fields = [
"workspace",
"page",
]
read_only_fields = ["workspace", "page"]
def get_entity_details(self, obj):
entity_name = obj.entity_name
@@ -158,10 +144,7 @@ class PageLogSerializer(BaseSerializer):
class Meta:
model = PageLog
fields = "__all__"
read_only_fields = [
"workspace",
"page",
]
read_only_fields = ["workspace", "page"]
class PageVersionSerializer(BaseSerializer):
@@ -178,10 +161,7 @@ class PageVersionSerializer(BaseSerializer):
"created_by",
"updated_by",
]
read_only_fields = [
"workspace",
"page",
]
read_only_fields = ["workspace", "page"]
class PageVersionDetailSerializer(BaseSerializer):
@@ -201,7 +181,4 @@ class PageVersionDetailSerializer(BaseSerializer):
"created_by",
"updated_by",
]
read_only_fields = [
"workspace",
"page",
]
read_only_fields = ["workspace", "page"]
+11 -33
View File
@@ -4,10 +4,7 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer, DynamicBaseSerializer
from plane.app.serializers.workspace import WorkspaceLiteSerializer
from plane.app.serializers.user import (
UserLiteSerializer,
UserAdminLiteSerializer,
)
from plane.app.serializers.user import UserLiteSerializer, UserAdminLiteSerializer
from plane.db.models import (
Project,
ProjectMember,
@@ -19,31 +16,23 @@ from plane.db.models import (
class ProjectSerializer(BaseSerializer):
workspace_detail = WorkspaceLiteSerializer(
source="workspace", read_only=True
)
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
inbox_view = serializers.BooleanField(read_only=True, source="intake_view")
class Meta:
model = Project
fields = "__all__"
read_only_fields = [
"workspace",
"deleted_at",
]
read_only_fields = ["workspace", "deleted_at"]
def create(self, validated_data):
identifier = validated_data.get("identifier", "").strip().upper()
if identifier == "":
raise serializers.ValidationError(
detail="Project Identifier is required"
)
raise serializers.ValidationError(detail="Project Identifier is required")
if ProjectIdentifier.objects.filter(
name=identifier, workspace_id=self.context["workspace_id"]
).exists():
raise serializers.ValidationError(
detail="Project Identifier is taken"
)
raise serializers.ValidationError(detail="Project Identifier is taken")
project = Project.objects.create(
**validated_data, workspace_id=self.context["workspace_id"]
)
@@ -82,9 +71,7 @@ class ProjectSerializer(BaseSerializer):
return project
# If not same fail update
raise serializers.ValidationError(
detail="Project Identifier is already taken"
)
raise serializers.ValidationError(detail="Project Identifier is already taken")
class ProjectLiteSerializer(BaseSerializer):
@@ -119,6 +106,7 @@ class ProjectListSerializer(DynamicBaseSerializer):
anchor = serializers.CharField(read_only=True)
members = serializers.SerializerMethodField()
cover_image_url = serializers.CharField(read_only=True)
inbox_view = serializers.BooleanField(read_only=True, source="intake_view")
def get_members(self, obj):
project_members = getattr(obj, "members_list", None)
@@ -212,26 +200,16 @@ class ProjectMemberLiteSerializer(BaseSerializer):
class DeployBoardSerializer(BaseSerializer):
project_details = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(
read_only=True, source="workspace"
)
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
class Meta:
model = DeployBoard
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"anchor",
]
read_only_fields = ["workspace", "project", "anchor"]
class ProjectPublicMemberSerializer(BaseSerializer):
class Meta:
model = ProjectPublicMember
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"member",
]
read_only_fields = ["workspace", "project", "member"]
+2 -10
View File
@@ -19,19 +19,11 @@ class StateSerializer(BaseSerializer):
"description",
"sequence",
]
read_only_fields = [
"workspace",
"project",
]
read_only_fields = ["workspace", "project"]
class StateLiteSerializer(BaseSerializer):
class Meta:
model = State
fields = [
"id",
"name",
"color",
"group",
]
fields = ["id", "name", "color", "group"]
read_only_fields = fields
+11 -43
View File
@@ -2,13 +2,7 @@
from rest_framework import serializers
# Module import
from plane.db.models import (
Account,
Profile,
User,
Workspace,
WorkspaceMemberInvite,
)
from plane.db.models import Account, Profile, User, Workspace, WorkspaceMemberInvite
from .base import BaseSerializer
@@ -17,11 +11,7 @@ class UserSerializer(BaseSerializer):
class Meta:
model = User
# Exclude password field from the serializer
fields = [
field.name
for field in User._meta.fields
if field.name != "password"
]
fields = [field.name for field in User._meta.fields if field.name != "password"]
# Make all system fields and email read only
read_only_fields = [
"id",
@@ -56,7 +46,6 @@ class UserSerializer(BaseSerializer):
class UserMeSerializer(BaseSerializer):
class Meta:
model = User
fields = [
@@ -87,11 +76,7 @@ class UserMeSettingsSerializer(BaseSerializer):
class Meta:
model = User
fields = [
"id",
"email",
"workspace",
]
fields = ["id", "email", "workspace"]
read_only_fields = fields
def get_workspace(self, obj):
@@ -128,8 +113,7 @@ class UserMeSettingsSerializer(BaseSerializer):
else:
fallback_workspace = (
Workspace.objects.filter(
workspace_member__member_id=obj.id,
workspace_member__is_active=True,
workspace_member__member_id=obj.id, workspace_member__is_active=True
)
.order_by("created_at")
.first()
@@ -138,14 +122,10 @@ class UserMeSettingsSerializer(BaseSerializer):
"last_workspace_id": None,
"last_workspace_slug": None,
"fallback_workspace_id": (
fallback_workspace.id
if fallback_workspace is not None
else None
fallback_workspace.id if fallback_workspace is not None else None
),
"fallback_workspace_slug": (
fallback_workspace.slug
if fallback_workspace is not None
else None
fallback_workspace.slug if fallback_workspace is not None else None
),
"invites": workspace_invites,
}
@@ -163,10 +143,7 @@ class UserLiteSerializer(BaseSerializer):
"is_bot",
"display_name",
]
read_only_fields = [
"id",
"is_bot",
]
read_only_fields = ["id", "is_bot"]
class UserAdminLiteSerializer(BaseSerializer):
@@ -183,10 +160,7 @@ class UserAdminLiteSerializer(BaseSerializer):
"email",
"last_login_medium",
]
read_only_fields = [
"id",
"is_bot",
]
read_only_fields = ["id", "is_bot"]
class ChangePasswordSerializer(serializers.Serializer):
@@ -207,9 +181,7 @@ class ChangePasswordSerializer(serializers.Serializer):
if data.get("new_password") != data.get("confirm_password"):
raise serializers.ValidationError(
{
"error": "Confirm password should be same as the new password."
}
{"error": "Confirm password should be same as the new password."}
)
return data
@@ -227,15 +199,11 @@ class ProfileSerializer(BaseSerializer):
class Meta:
model = Profile
fields = "__all__"
read_only_fields = [
"user",
]
read_only_fields = ["user"]
class AccountSerializer(BaseSerializer):
class Meta:
model = Account
fields = "__all__"
read_only_fields = [
"user",
]
read_only_fields = ["user"]
+4 -13
View File
@@ -47,13 +47,9 @@ class WebhookSerializer(DynamicBaseSerializer):
# Additional validation for multiple request domains and their subdomains
request = self.context.get("request")
disallowed_domains = [
"plane.so",
] # Add your disallowed domains here
disallowed_domains = ["plane.so"] # Add your disallowed domains here
if request:
request_host = request.get_host().split(":")[
0
] # Remove port if present
request_host = request.get_host().split(":")[0] # Remove port if present
disallowed_domains.append(request_host)
# Check if hostname is a subdomain or exact match of any disallowed domain
@@ -99,9 +95,7 @@ class WebhookSerializer(DynamicBaseSerializer):
# Additional validation for multiple request domains and their subdomains
request = self.context.get("request")
disallowed_domains = [
"plane.so",
] # Add your disallowed domains here
disallowed_domains = ["plane.so"] # Add your disallowed domains here
if request:
request_host = request.get_host().split(":")[
0
@@ -122,10 +116,7 @@ class WebhookSerializer(DynamicBaseSerializer):
class Meta:
model = Webhook
fields = "__all__"
read_only_fields = [
"workspace",
"secret_key",
]
read_only_fields = ["workspace", "secret_key"]
class WebhookLogSerializer(DynamicBaseSerializer):
+6 -19
View File
@@ -47,11 +47,7 @@ class WorkSpaceSerializer(DynamicBaseSerializer):
class WorkspaceLiteSerializer(BaseSerializer):
class Meta:
model = Workspace
fields = [
"name",
"slug",
"id",
]
fields = ["name", "slug", "id"]
read_only_fields = fields
@@ -66,6 +62,7 @@ class WorkSpaceMemberSerializer(DynamicBaseSerializer):
class WorkspaceMemberMeSerializer(BaseSerializer):
draft_issue_count = serializers.IntegerField(read_only=True)
class Meta:
model = WorkspaceMember
fields = "__all__"
@@ -101,9 +98,7 @@ class WorkSpaceMemberInviteSerializer(BaseSerializer):
class TeamSerializer(BaseSerializer):
members_detail = UserLiteSerializer(
read_only=True, source="members", many=True
)
members_detail = UserLiteSerializer(read_only=True, source="members", many=True)
members = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
write_only=True,
@@ -140,9 +135,7 @@ class TeamSerializer(BaseSerializer):
members = validated_data.pop("members")
TeamMember.objects.filter(team=instance).delete()
team_members = [
TeamMember(
member=member, team=instance, workspace=instance.workspace
)
TeamMember(member=member, team=instance, workspace=instance.workspace)
for member in members
]
TeamMember.objects.bulk_create(team_members, batch_size=10)
@@ -154,17 +147,11 @@ class WorkspaceThemeSerializer(BaseSerializer):
class Meta:
model = WorkspaceTheme
fields = "__all__"
read_only_fields = [
"workspace",
"actor",
]
read_only_fields = ["workspace", "actor"]
class WorkspaceUserPropertiesSerializer(BaseSerializer):
class Meta:
model = WorkspaceUserProperties
fields = "__all__"
read_only_fields = [
"workspace",
"user",
]
read_only_fields = ["workspace", "user"]
+2 -2
View File
@@ -5,7 +5,7 @@ from .cycle import urlpatterns as cycle_urls
from .dashboard import urlpatterns as dashboard_urls
from .estimate import urlpatterns as estimate_urls
from .external import urlpatterns as external_urls
from .inbox import urlpatterns as inbox_urls
from .intake import urlpatterns as intake_urls
from .issue import urlpatterns as issue_urls
from .module import urlpatterns as module_urls
from .notification import urlpatterns as notification_urls
@@ -25,7 +25,7 @@ urlpatterns = [
*dashboard_urls,
*estimate_urls,
*external_urls,
*inbox_urls,
*intake_urls,
*issue_urls,
*module_urls,
*notification_urls,
+2 -10
View File
@@ -26,11 +26,7 @@ urlpatterns = [
FileAssetEndpoint.as_view(),
name="file-assets",
),
path(
"users/file-assets/",
UserAssetsEndpoint.as_view(),
name="user-file-assets",
),
path("users/file-assets/", UserAssetsEndpoint.as_view(), name="user-file-assets"),
path(
"users/file-assets/<str:asset_key>/",
UserAssetsEndpoint.as_view(),
@@ -38,11 +34,7 @@ urlpatterns = [
),
path(
"workspaces/file-assets/<uuid:workspace_id>/<str:asset_key>/restore/",
FileAssetViewSet.as_view(
{
"post": "restore",
}
),
FileAssetViewSet.as_view({"post": "restore"}),
name="file-assets-restore",
),
# V2 Endpoints
+4 -23
View File
@@ -17,12 +17,7 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/",
CycleViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
CycleViewSet.as_view({"get": "list", "post": "create"}),
name="project-cycle",
),
path(
@@ -39,12 +34,7 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/",
CycleIssueViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
CycleIssueViewSet.as_view({"get": "list", "post": "create"}),
name="project-issue-cycle",
),
path(
@@ -66,21 +56,12 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-cycles/",
CycleFavoriteViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
CycleFavoriteViewSet.as_view({"get": "list", "post": "create"}),
name="user-favorite-cycle",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-cycles/<uuid:cycle_id>/",
CycleFavoriteViewSet.as_view(
{
"delete": "destroy",
}
),
CycleFavoriteViewSet.as_view({"delete": "destroy"}),
name="user-favorite-cycle",
),
path(
+4 -22
View File
@@ -16,42 +16,24 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
BulkEstimatePointEndpoint.as_view(
{
"get": "list",
"post": "create",
}
),
BulkEstimatePointEndpoint.as_view({"get": "list", "post": "create"}),
name="bulk-create-estimate-points",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/",
BulkEstimatePointEndpoint.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="bulk-create-estimate-points",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/estimate-points/",
EstimatePointEndpoint.as_view(
{
"post": "create",
}
),
EstimatePointEndpoint.as_view({"post": "create"}),
name="estimate-points",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/estimate-points/<estimate_point_id>/",
EstimatePointEndpoint.as_view(
{
"patch": "partial_update",
"delete": "destroy",
}
),
EstimatePointEndpoint.as_view({"patch": "partial_update", "delete": "destroy"}),
name="estimate-points",
),
]
+2 -6
View File
@@ -6,17 +6,13 @@ from plane.app.views import GPTIntegrationEndpoint, WorkspaceGPTIntegrationEndpo
urlpatterns = [
path(
"unsplash/",
UnsplashEndpoint.as_view(),
name="unsplash",
),
path("unsplash/", UnsplashEndpoint.as_view(), name="unsplash"),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/ai-assistant/",
GPTIntegrationEndpoint.as_view(),
name="importer",
),
path(
path(
"workspaces/<str:slug>/ai-assistant/",
WorkspaceGPTIntegrationEndpoint.as_view(),
name="importer",
-53
View File
@@ -1,53 +0,0 @@
from django.urls import path
from plane.app.views import (
InboxViewSet,
InboxIssueViewSet,
)
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/",
InboxViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="inbox",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:pk>/",
InboxViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="inbox",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/",
InboxIssueViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="inbox-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:pk>/",
InboxIssueViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="inbox-issue",
),
]
+56
View File
@@ -0,0 +1,56 @@
from django.urls import path
from plane.app.views import IntakeViewSet, IntakeIssueViewSet
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/intakes/",
IntakeViewSet.as_view({"get": "list", "post": "create"}),
name="intake",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/intakes/<uuid:pk>/",
IntakeViewSet.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="intake",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-issues/",
IntakeIssueViewSet.as_view({"get": "list", "post": "create"}),
name="intake-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-issues/<uuid:pk>/",
IntakeIssueViewSet.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="intake-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/",
IntakeViewSet.as_view({"get": "list", "post": "create"}),
name="inbox",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:pk>/",
IntakeViewSet.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="inbox",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/",
IntakeIssueViewSet.as_view({"get": "list", "post": "create"}),
name="inbox-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:pk>/",
IntakeIssueViewSet.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="inbox-issue",
),
]
+27 -78
View File
@@ -21,7 +21,9 @@ from plane.app.views import (
BulkArchiveIssuesEndpoint,
DeletedIssuesListViewSet,
IssuePaginatedViewSet,
IssueDetailEndpoint,
IssueAttachmentV2Endpoint,
IssueBulkUpdateDateEndpoint,
)
urlpatterns = [
@@ -32,14 +34,15 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/",
IssueViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
IssueViewSet.as_view({"get": "list", "post": "create"}),
name="project-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues-detail/",
IssueDetailEndpoint.as_view(),
name="project-issue-detail",
),
# updated v1 paginated issues
# updated v2 paginated issues
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/v2/issues/",
@@ -60,12 +63,7 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-labels/",
LabelViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
LabelViewSet.as_view({"get": "list", "post": "create"}),
name="project-issue-labels",
),
path(
@@ -103,12 +101,7 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-links/",
IssueLinkViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
IssueLinkViewSet.as_view({"get": "list", "post": "create"}),
name="project-issue-links",
),
path(
@@ -161,12 +154,7 @@ urlpatterns = [
## IssueComments
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/comments/",
IssueCommentViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
IssueCommentViewSet.as_view({"get": "list", "post": "create"}),
name="project-issue-comment",
),
path(
@@ -185,12 +173,7 @@ urlpatterns = [
# Issue Subscribers
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-subscribers/",
IssueSubscriberViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
IssueSubscriberViewSet.as_view({"get": "list", "post": "create"}),
name="project-issue-subscribers",
),
path(
@@ -201,11 +184,7 @@ urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/subscribe/",
IssueSubscriberViewSet.as_view(
{
"get": "subscription_status",
"post": "subscribe",
"delete": "unsubscribe",
}
{"get": "subscription_status", "post": "subscribe", "delete": "unsubscribe"}
),
name="project-issue-subscribers",
),
@@ -213,42 +192,24 @@ urlpatterns = [
# Issue Reactions
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/reactions/",
IssueReactionViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
IssueReactionViewSet.as_view({"get": "list", "post": "create"}),
name="project-issue-reactions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/reactions/<str:reaction_code>/",
IssueReactionViewSet.as_view(
{
"delete": "destroy",
}
),
IssueReactionViewSet.as_view({"delete": "destroy"}),
name="project-issue-reactions",
),
## End Issue Reactions
# Comment Reactions
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/comments/<uuid:comment_id>/reactions/",
CommentReactionViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
CommentReactionViewSet.as_view({"get": "list", "post": "create"}),
name="project-issue-comment-reactions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/comments/<uuid:comment_id>/reactions/<str:reaction_code>/",
CommentReactionViewSet.as_view(
{
"delete": "destroy",
}
),
CommentReactionViewSet.as_view({"delete": "destroy"}),
name="project-issue-comment-reactions",
),
## End Comment Reactions
@@ -262,21 +223,13 @@ urlpatterns = [
## Issue Archives
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/",
IssueArchiveViewSet.as_view(
{
"get": "list",
}
),
IssueArchiveViewSet.as_view({"get": "list"}),
name="project-issue-archive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:pk>/archive/",
IssueArchiveViewSet.as_view(
{
"get": "retrieve",
"post": "archive",
"delete": "unarchive",
}
{"get": "retrieve", "post": "archive", "delete": "unarchive"}
),
name="project-issue-archive-unarchive",
),
@@ -284,21 +237,12 @@ urlpatterns = [
## Issue Relation
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/",
IssueRelationViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
IssueRelationViewSet.as_view({"get": "list", "post": "create"}),
name="issue-relation",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/remove-relation/",
IssueRelationViewSet.as_view(
{
"post": "remove_relation",
}
),
IssueRelationViewSet.as_view({"post": "remove_relation"}),
name="issue-relation",
),
## End Issue Relation
@@ -307,4 +251,9 @@ urlpatterns = [
DeletedIssuesListViewSet.as_view(),
name="deleted-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-dates/",
IssueBulkUpdateDateEndpoint.as_view(),
name="project-issue-dates",
),
]
+6 -34
View File
@@ -14,12 +14,7 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/",
ModuleViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
ModuleViewSet.as_view({"get": "list", "post": "create"}),
name="project-modules",
),
path(
@@ -36,21 +31,12 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/modules/",
ModuleIssueViewSet.as_view(
{
"post": "create_issue_modules",
}
),
ModuleIssueViewSet.as_view({"post": "create_issue_modules"}),
name="issue-module",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/issues/",
ModuleIssueViewSet.as_view(
{
"post": "create_module_issues",
"get": "list",
}
),
ModuleIssueViewSet.as_view({"post": "create_module_issues", "get": "list"}),
name="project-module-issues",
),
path(
@@ -67,12 +53,7 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-links/",
ModuleLinkViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
ModuleLinkViewSet.as_view({"get": "list", "post": "create"}),
name="project-issue-module-links",
),
path(
@@ -89,21 +70,12 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-modules/",
ModuleFavoriteViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
ModuleFavoriteViewSet.as_view({"get": "list", "post": "create"}),
name="user-favorite-module",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-modules/<uuid:module_id>/",
ModuleFavoriteViewSet.as_view(
{
"delete": "destroy",
}
),
ModuleFavoriteViewSet.as_view({"delete": "destroy"}),
name="user-favorite-module",
),
path(
+5 -27
View File
@@ -12,42 +12,24 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/users/notifications/",
NotificationViewSet.as_view(
{
"get": "list",
}
),
NotificationViewSet.as_view({"get": "list"}),
name="notifications",
),
path(
"workspaces/<str:slug>/users/notifications/<uuid:pk>/",
NotificationViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="notifications",
),
path(
"workspaces/<str:slug>/users/notifications/<uuid:pk>/read/",
NotificationViewSet.as_view(
{
"post": "mark_read",
"delete": "mark_unread",
}
),
NotificationViewSet.as_view({"post": "mark_read", "delete": "mark_unread"}),
name="notifications",
),
path(
"workspaces/<str:slug>/users/notifications/<uuid:pk>/archive/",
NotificationViewSet.as_view(
{
"post": "archive",
"delete": "unarchive",
}
),
NotificationViewSet.as_view({"post": "archive", "delete": "unarchive"}),
name="notifications",
),
path(
@@ -57,11 +39,7 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/users/notifications/mark-all-read/",
MarkAllReadNotificationViewSet.as_view(
{
"post": "create",
}
),
MarkAllReadNotificationViewSet.as_view({"post": "create"}),
name="mark-all-read-notifications",
),
path(
+7 -40
View File
@@ -14,66 +14,38 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/",
PageViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
PageViewSet.as_view({"get": "list", "post": "create"}),
name="project-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/",
PageViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="project-pages",
),
# favorite pages
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/favorite-pages/<uuid:pk>/",
PageFavoriteViewSet.as_view(
{
"post": "create",
"delete": "destroy",
}
),
PageFavoriteViewSet.as_view({"post": "create", "delete": "destroy"}),
name="user-favorite-pages",
),
# archived pages
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/archive/",
PageViewSet.as_view(
{
"post": "archive",
"delete": "unarchive",
}
),
PageViewSet.as_view({"post": "archive", "delete": "unarchive"}),
name="project-page-archive-unarchive",
),
# lock and unlock
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/lock/",
PageViewSet.as_view(
{
"post": "lock",
"delete": "unlock",
}
),
PageViewSet.as_view({"post": "lock", "delete": "unlock"}),
name="project-pages-lock-unlock",
),
# private and public page
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/access/",
PageViewSet.as_view(
{
"post": "access",
}
),
PageViewSet.as_view({"post": "access"}),
name="project-pages-access",
),
path(
@@ -93,12 +65,7 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/description/",
PagesDescriptionViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
}
),
PagesDescriptionViewSet.as_view({"get": "retrieve", "patch": "partial_update"}),
name="page-description",
),
path(
+11 -62
View File
@@ -21,12 +21,7 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/",
ProjectViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
ProjectViewSet.as_view({"get": "list", "post": "create"}),
name="project",
),
path(
@@ -48,32 +43,17 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/invitations/",
ProjectInvitationsViewset.as_view(
{
"get": "list",
"post": "create",
},
),
ProjectInvitationsViewset.as_view({"get": "list", "post": "create"}),
name="project-member-invite",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/invitations/<uuid:pk>/",
ProjectInvitationsViewset.as_view(
{
"get": "retrieve",
"delete": "destroy",
}
),
ProjectInvitationsViewset.as_view({"get": "retrieve", "delete": "destroy"}),
name="project-member-invite",
),
path(
"users/me/workspaces/<str:slug>/projects/invitations/",
UserProjectInvitationsViewset.as_view(
{
"get": "list",
"post": "create",
},
),
UserProjectInvitationsViewset.as_view({"get": "list", "post": "create"}),
name="user-project-invitations",
),
path(
@@ -88,32 +68,19 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/members/",
ProjectMemberViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
ProjectMemberViewSet.as_view({"get": "list", "post": "create"}),
name="project-member",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/members/<uuid:pk>/",
ProjectMemberViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="project-member",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/members/leave/",
ProjectMemberViewSet.as_view(
{
"post": "leave",
}
),
ProjectMemberViewSet.as_view({"post": "leave"}),
name="project-member",
),
path(
@@ -133,21 +100,12 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/user-favorite-projects/",
ProjectFavoritesViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
ProjectFavoritesViewSet.as_view({"get": "list", "post": "create"}),
name="project-favorite",
),
path(
"workspaces/<str:slug>/user-favorite-projects/<uuid:project_id>/",
ProjectFavoritesViewSet.as_view(
{
"delete": "destroy",
}
),
ProjectFavoritesViewSet.as_view({"delete": "destroy"}),
name="project-favorite",
),
path(
@@ -157,22 +115,13 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/",
DeployBoardViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
DeployBoardViewSet.as_view({"get": "list", "post": "create"}),
name="project-deploy-board",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/<uuid:pk>/",
DeployBoardViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="project-deploy-board",
),
+1 -4
View File
@@ -1,10 +1,7 @@
from django.urls import path
from plane.app.views import (
GlobalSearchEndpoint,
IssueSearchEndpoint,
)
from plane.app.views import GlobalSearchEndpoint, IssueSearchEndpoint
urlpatterns = [
+3 -16
View File
@@ -7,32 +7,19 @@ from plane.app.views import StateViewSet
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/states/",
StateViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
StateViewSet.as_view({"get": "list", "post": "create"}),
name="project-states",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/states/<uuid:pk>/",
StateViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="project-state",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/states/<uuid:pk>/mark-default/",
StateViewSet.as_view(
{
"post": "mark_as_default",
}
),
StateViewSet.as_view({"post": "mark_as_default"}),
name="project-state",
),
]
+10 -44
View File
@@ -22,60 +22,30 @@ urlpatterns = [
path(
"users/me/",
UserEndpoint.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "deactivate",
}
{"get": "retrieve", "patch": "partial_update", "delete": "deactivate"}
),
name="users",
),
path(
"users/session/",
UserSessionEndpoint.as_view(),
name="user-session",
),
path("users/session/", UserSessionEndpoint.as_view(), name="user-session"),
path(
"users/me/settings/",
UserEndpoint.as_view(
{
"get": "retrieve_user_settings",
}
),
UserEndpoint.as_view({"get": "retrieve_user_settings"}),
name="users",
),
# Profile
path(
"users/me/profile/",
ProfileEndpoint.as_view(),
name="accounts",
),
path("users/me/profile/", ProfileEndpoint.as_view(), name="accounts"),
# End profile
# Accounts
path(
"users/me/accounts/",
AccountEndpoint.as_view(),
name="accounts",
),
path(
"users/me/accounts/<uuid:pk>/",
AccountEndpoint.as_view(),
name="accounts",
),
path("users/me/accounts/", AccountEndpoint.as_view(), name="accounts"),
path("users/me/accounts/<uuid:pk>/", AccountEndpoint.as_view(), name="accounts"),
## End Accounts
path(
"users/me/instance-admin/",
UserEndpoint.as_view(
{
"get": "retrieve_instance_admin",
}
),
UserEndpoint.as_view({"get": "retrieve_instance_admin"}),
name="users",
),
path(
"users/me/onboard/",
UpdateUserOnBoardedEndpoint.as_view(),
name="user-onboard",
"users/me/onboard/", UpdateUserOnBoardedEndpoint.as_view(), name="user-onboard"
),
path(
"users/me/tour-completed/",
@@ -83,15 +53,11 @@ urlpatterns = [
name="user-tour",
),
path(
"users/me/activities/",
UserActivityEndpoint.as_view(),
name="user-activities",
"users/me/activities/", UserActivityEndpoint.as_view(), name="user-activities"
),
# user workspaces
path(
"users/me/workspaces/",
UserWorkSpacesEndpoint.as_view(),
name="user-workspace",
"users/me/workspaces/", UserWorkSpacesEndpoint.as_view(), name="user-workspace"
),
# User Graphs
path(
+5 -28
View File
@@ -12,12 +12,7 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/views/",
IssueViewViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
IssueViewViewSet.as_view({"get": "list", "post": "create"}),
name="project-view",
),
path(
@@ -34,12 +29,7 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/views/",
WorkspaceViewViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
WorkspaceViewViewSet.as_view({"get": "list", "post": "create"}),
name="global-view",
),
path(
@@ -56,30 +46,17 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/issues/",
WorkspaceViewIssuesViewSet.as_view(
{
"get": "list",
}
),
WorkspaceViewIssuesViewSet.as_view({"get": "list"}),
name="global-view-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/",
IssueViewFavoriteViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
IssueViewFavoriteViewSet.as_view({"get": "list", "post": "create"}),
name="user-favorite-view",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/<uuid:view_id>/",
IssueViewFavoriteViewSet.as_view(
{
"delete": "destroy",
}
),
IssueViewFavoriteViewSet.as_view({"delete": "destroy"}),
name="user-favorite-view",
),
]
+1 -5
View File
@@ -8,11 +8,7 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/webhooks/",
WebhookEndpoint.as_view(),
name="webhooks",
),
path("workspaces/<str:slug>/webhooks/", WebhookEndpoint.as_view(), name="webhooks"),
path(
"workspaces/<str:slug>/webhooks/<uuid:pk>/",
WebhookEndpoint.as_view(),
+11 -61
View File
@@ -39,12 +39,7 @@ urlpatterns = [
),
path(
"workspaces/",
WorkSpaceViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
WorkSpaceViewSet.as_view({"get": "list", "post": "create"}),
name="workspace",
),
path(
@@ -61,34 +56,20 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/invitations/",
WorkspaceInvitationsViewset.as_view(
{
"get": "list",
"post": "create",
},
),
WorkspaceInvitationsViewset.as_view({"get": "list", "post": "create"}),
name="workspace-invitations",
),
path(
"workspaces/<str:slug>/invitations/<uuid:pk>/",
WorkspaceInvitationsViewset.as_view(
{
"delete": "destroy",
"get": "retrieve",
"patch": "partial_update",
}
{"delete": "destroy", "get": "retrieve", "patch": "partial_update"}
),
name="workspace-invitations",
),
# user workspace invitations
path(
"users/me/workspaces/invitations/",
UserWorkspaceInvitationsViewSet.as_view(
{
"get": "list",
"post": "create",
},
),
UserWorkspaceInvitationsViewSet.as_view({"get": "list", "post": "create"}),
name="user-workspace-invitations",
),
path(
@@ -110,31 +91,18 @@ urlpatterns = [
path(
"workspaces/<str:slug>/members/<uuid:pk>/",
WorkSpaceMemberViewSet.as_view(
{
"patch": "partial_update",
"delete": "destroy",
"get": "retrieve",
}
{"patch": "partial_update", "delete": "destroy", "get": "retrieve"}
),
name="workspace-member",
),
path(
"workspaces/<str:slug>/members/leave/",
WorkSpaceMemberViewSet.as_view(
{
"post": "leave",
},
),
WorkSpaceMemberViewSet.as_view({"post": "leave"}),
name="leave-workspace-members",
),
path(
"workspaces/<str:slug>/teams/",
TeamMemberViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
TeamMemberViewSet.as_view({"get": "list", "post": "create"}),
name="workspace-team-members",
),
path(
@@ -166,22 +134,13 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/workspace-themes/",
WorkspaceThemeViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
WorkspaceThemeViewSet.as_view({"get": "list", "post": "create"}),
name="workspace-themes",
),
path(
"workspaces/<str:slug>/workspace-themes/<uuid:pk>/",
WorkspaceThemeViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="workspace-themes",
),
@@ -257,22 +216,13 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/draft-issues/",
WorkspaceDraftIssueViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
WorkspaceDraftIssueViewSet.as_view({"get": "list", "post": "create"}),
name="workspace-draft-issues",
),
path(
"workspaces/<str:slug>/draft-issues/<uuid:pk>/",
WorkspaceDraftIssueViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="workspace-drafts-issues",
),
+22 -63
View File
@@ -59,12 +59,8 @@ from .workspace.invite import (
WorkspaceJoinEndpoint,
UserWorkspaceInvitationsViewSet,
)
from .workspace.label import (
WorkspaceLabelsEndpoint,
)
from .workspace.state import (
WorkspaceStatesEndpoint,
)
from .workspace.label import WorkspaceLabelsEndpoint
from .workspace.state import WorkspaceStatesEndpoint
from .workspace.user import (
UserLastProjectWithWorkspaceEndpoint,
WorkspaceUserProfileIssuesEndpoint,
@@ -75,15 +71,9 @@ from .workspace.user import (
UserActivityGraphEndpoint,
UserIssueCompletedGraphEndpoint,
)
from .workspace.estimate import (
WorkspaceEstimatesEndpoint,
)
from .workspace.module import (
WorkspaceModulesEndpoint,
)
from .workspace.cycle import (
WorkspaceCyclesEndpoint,
)
from .workspace.estimate import WorkspaceEstimatesEndpoint
from .workspace.module import WorkspaceModulesEndpoint
from .workspace.cycle import WorkspaceCyclesEndpoint
from .state.base import StateViewSet
from .view.base import (
@@ -103,18 +93,10 @@ from .cycle.base import (
CycleAnalyticsEndpoint,
CycleProgressEndpoint,
)
from .cycle.issue import (
CycleIssueViewSet,
)
from .cycle.archive import (
CycleArchiveUnarchiveEndpoint,
)
from .cycle.issue import CycleIssueViewSet
from .cycle.archive import CycleArchiveUnarchiveEndpoint
from .asset.base import (
FileAssetEndpoint,
UserAssetsEndpoint,
FileAssetViewSet,
)
from .asset.base import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet
from .asset.v2 import (
WorkspaceFileAssetEndpoint,
UserAssetsV2Endpoint,
@@ -130,11 +112,11 @@ from .issue.base import (
BulkDeleteIssuesEndpoint,
DeletedIssuesListViewSet,
IssuePaginatedViewSet,
IssueDetailEndpoint,
IssueBulkUpdateDateEndpoint,
)
from .issue.activity import (
IssueActivityEndpoint,
)
from .issue.activity import IssueActivityEndpoint
from .issue.archive import IssueArchiveViewSet, BulkArchiveIssuesEndpoint
@@ -144,35 +126,19 @@ from .issue.attachment import (
IssueAttachmentV2Endpoint,
)
from .issue.comment import (
IssueCommentViewSet,
CommentReactionViewSet,
)
from .issue.comment import IssueCommentViewSet, CommentReactionViewSet
from .issue.label import (
LabelViewSet,
BulkCreateIssueLabelsEndpoint,
)
from .issue.label import LabelViewSet, BulkCreateIssueLabelsEndpoint
from .issue.link import (
IssueLinkViewSet,
)
from .issue.link import IssueLinkViewSet
from .issue.relation import (
IssueRelationViewSet,
)
from .issue.relation import IssueRelationViewSet
from .issue.reaction import (
IssueReactionViewSet,
)
from .issue.reaction import IssueReactionViewSet
from .issue.sub_issue import (
SubIssuesEndpoint,
)
from .issue.sub_issue import SubIssuesEndpoint
from .issue.subscriber import (
IssueSubscriberViewSet,
)
from .issue.subscriber import IssueSubscriberViewSet
from .module.base import (
ModuleViewSet,
@@ -181,18 +147,11 @@ from .module.base import (
ModuleUserPropertiesEndpoint,
)
from .module.issue import (
ModuleIssueViewSet,
)
from .module.issue import ModuleIssueViewSet
from .module.archive import (
ModuleArchiveUnarchiveEndpoint,
)
from .module.archive import ModuleArchiveUnarchiveEndpoint
from .api import (
ApiTokenEndpoint,
ServiceApiTokenEndpoint,
)
from .api import ApiTokenEndpoint, ServiceApiTokenEndpoint
from .page.base import (
PageViewSet,
@@ -218,7 +177,7 @@ from .estimate.base import (
EstimatePointEndpoint,
)
from .inbox.base import InboxViewSet, InboxIssueViewSet
from .intake.base import IntakeViewSet, IntakeIssueViewSet
from .analytic.base import (
AnalyticsEndpoint,
+26 -79
View File
@@ -22,14 +22,7 @@ from plane.app.permissions import allow_permission, ROLE
class AnalyticsEndpoint(BaseAPIView):
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
],
level="WORKSPACE",
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def get(self, request, slug):
x_axis = request.GET.get("x_axis", False)
y_axis = request.GET.get("y_axis", False)
@@ -50,10 +43,7 @@ class AnalyticsEndpoint(BaseAPIView):
"completed_at",
]
valid_yaxis = [
"issue_count",
"estimate",
]
valid_yaxis = ["issue_count", "estimate"]
# Check for x-axis and y-axis as thery are required parameters
if (
@@ -70,9 +60,7 @@ class AnalyticsEndpoint(BaseAPIView):
)
# If segment is present it cannot be same as x-axis
if segment and (
segment not in valid_xaxis_segment or x_axis == segment
):
if segment and (segment not in valid_xaxis_segment or x_axis == segment):
return Response(
{
"error": "Both segment and x axis cannot be same and segment should be valid"
@@ -97,10 +85,7 @@ class AnalyticsEndpoint(BaseAPIView):
state_details = {}
if x_axis in ["state_id"] or segment in ["state_id"]:
state_details = (
Issue.issue_objects.filter(
workspace__slug=slug,
**filters,
)
Issue.issue_objects.filter(workspace__slug=slug, **filters)
.distinct("state_id")
.order_by("state_id")
.values("state_id", "state__name", "state__color")
@@ -110,7 +95,10 @@ class AnalyticsEndpoint(BaseAPIView):
if x_axis in ["labels__id"] or segment in ["labels__id"]:
label_details = (
Issue.objects.filter(
workspace__slug=slug, **filters, labels__id__isnull=False
workspace__slug=slug,
**filters,
labels__id__isnull=False,
label_issue__deleted_at__isnull=True,
)
.distinct("labels__id")
.order_by("labels__id")
@@ -160,21 +148,17 @@ class AnalyticsEndpoint(BaseAPIView):
)
cycle_details = {}
if x_axis in ["issue_cycle__cycle_id"] or segment in [
"issue_cycle__cycle_id"
]:
if x_axis in ["issue_cycle__cycle_id"] or segment in ["issue_cycle__cycle_id"]:
cycle_details = (
Issue.issue_objects.filter(
workspace__slug=slug,
**filters,
issue_cycle__cycle_id__isnull=False,
issue_cycle__deleted_at__isnull=True,
)
.distinct("issue_cycle__cycle_id")
.order_by("issue_cycle__cycle_id")
.values(
"issue_cycle__cycle_id",
"issue_cycle__cycle__name",
)
.values("issue_cycle__cycle_id", "issue_cycle__cycle__name")
)
module_details = {}
@@ -186,13 +170,11 @@ class AnalyticsEndpoint(BaseAPIView):
workspace__slug=slug,
**filters,
issue_module__module_id__isnull=False,
issue_module__deleted_at__isnull=True,
)
.distinct("issue_module__module_id")
.order_by("issue_module__module_id")
.values(
"issue_module__module_id",
"issue_module__module__name",
)
.values("issue_module__module_id", "issue_module__module__name")
)
return Response(
@@ -212,9 +194,7 @@ class AnalyticsEndpoint(BaseAPIView):
class AnalyticViewViewset(BaseViewSet):
permission_classes = [
WorkSpaceAdminPermission,
]
permission_classes = [WorkSpaceAdminPermission]
model = AnalyticView
serializer_class = AnalyticViewSerializer
@@ -224,25 +204,14 @@ class AnalyticViewViewset(BaseViewSet):
def get_queryset(self):
return self.filter_queryset(
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
super().get_queryset().filter(workspace__slug=self.kwargs.get("slug"))
)
class SavedAnalyticEndpoint(BaseAPIView):
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
],
level="WORKSPACE",
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def get(self, request, slug, analytic_id):
analytic_view = AnalyticView.objects.get(
pk=analytic_id, workspace__slug=slug
)
analytic_view = AnalyticView.objects.get(pk=analytic_id, workspace__slug=slug)
filter = analytic_view.query
queryset = Issue.issue_objects.filter(**filter)
@@ -268,14 +237,7 @@ class SavedAnalyticEndpoint(BaseAPIView):
class ExportAnalyticsEndpoint(BaseAPIView):
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
],
level="WORKSPACE",
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def post(self, request, slug):
x_axis = request.data.get("x_axis", False)
y_axis = request.data.get("y_axis", False)
@@ -296,10 +258,7 @@ class ExportAnalyticsEndpoint(BaseAPIView):
"completed_at",
]
valid_yaxis = [
"issue_count",
"estimate",
]
valid_yaxis = ["issue_count", "estimate"]
# Check for x-axis and y-axis as thery are required parameters
if (
@@ -316,9 +275,7 @@ class ExportAnalyticsEndpoint(BaseAPIView):
)
# If segment is present it cannot be same as x-axis
if segment and (
segment not in valid_xaxis_segment or x_axis == segment
):
if segment and (segment not in valid_xaxis_segment or x_axis == segment):
return Response(
{
"error": "Both segment and x axis cannot be same and segment should be valid"
@@ -339,13 +296,10 @@ class ExportAnalyticsEndpoint(BaseAPIView):
class DefaultAnalyticsEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def get(self, request, slug):
filters = issue_filters(request.GET, "GET")
base_issues = Issue.issue_objects.filter(
workspace__slug=slug, **filters
)
base_issues = Issue.issue_objects.filter(workspace__slug=slug, **filters)
total_issues = base_issues.count()
@@ -358,9 +312,7 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
)
open_issues_groups = ["backlog", "unstarted", "started"]
open_issues_queryset = state_groups.filter(
state__group__in=open_issues_groups
)
open_issues_queryset = state_groups.filter(state__group__in=open_issues_groups)
open_issues = open_issues_queryset.count()
open_issues_classified = (
@@ -402,8 +354,7 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
),
# If `avatar_asset` is None, fall back to using `avatar` field directly
When(
created_by__avatar_asset__isnull=True,
then="created_by__avatar",
created_by__avatar_asset__isnull=True, then="created_by__avatar"
),
default=Value(None),
output_field=models.CharField(),
@@ -436,8 +387,7 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
),
# If `avatar_asset` is None, fall back to using `avatar` field directly
When(
assignees__avatar_asset__isnull=True,
then="assignees__avatar",
assignees__avatar_asset__isnull=True, then="assignees__avatar"
),
default=Value(None),
output_field=models.CharField(),
@@ -464,8 +414,7 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
),
# If `avatar_asset` is None, fall back to using `avatar` field directly
When(
assignees__avatar_asset__isnull=True,
then="assignees__avatar",
assignees__avatar_asset__isnull=True, then="assignees__avatar"
),
default=Value(None),
output_field=models.CharField(),
@@ -474,9 +423,7 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
.order_by("-count")
)
open_estimate_sum = open_issues_queryset.aggregate(sum=Sum("point"))[
"sum"
]
open_estimate_sum = open_issues_queryset.aggregate(sum=Sum("point"))["sum"]
total_estimate_sum = base_issues.aggregate(sum=Sum("point"))["sum"]
return Response(
+9 -35
View File
@@ -13,9 +13,7 @@ from plane.app.permissions import WorkspaceOwnerPermission
class ApiTokenEndpoint(BaseAPIView):
permission_classes = [
WorkspaceOwnerPermission,
]
permission_classes = [WorkspaceOwnerPermission]
def post(self, request, slug):
label = request.data.get("label", str(uuid4().hex))
@@ -37,10 +35,7 @@ class ApiTokenEndpoint(BaseAPIView):
serializer = APITokenSerializer(api_token)
# Token will be only visible while creating
return Response(
serializer.data,
status=status.HTTP_201_CREATED,
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
def get(self, request, slug, pk=None):
if pk is None:
@@ -58,23 +53,14 @@ class ApiTokenEndpoint(BaseAPIView):
def delete(self, request, slug, pk):
api_token = APIToken.objects.get(
workspace__slug=slug,
user=request.user,
pk=pk,
is_service=False,
workspace__slug=slug, user=request.user, pk=pk, is_service=False
)
api_token.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
def patch(self, request, slug, pk):
api_token = APIToken.objects.get(
workspace__slug=slug,
user=request.user,
pk=pk,
)
serializer = APITokenSerializer(
api_token, data=request.data, partial=True
)
api_token = APIToken.objects.get(workspace__slug=slug, user=request.user, pk=pk)
serializer = APITokenSerializer(api_token, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
@@ -82,25 +68,17 @@ class ApiTokenEndpoint(BaseAPIView):
class ServiceApiTokenEndpoint(BaseAPIView):
permission_classes = [
WorkspaceOwnerPermission,
]
permission_classes = [WorkspaceOwnerPermission]
def post(self, request, slug):
workspace = Workspace.objects.get(slug=slug)
api_token = APIToken.objects.filter(
workspace=workspace,
is_service=True,
workspace=workspace, is_service=True
).first()
if api_token:
return Response(
{
"token": str(api_token.token),
},
status=status.HTTP_200_OK,
)
return Response({"token": str(api_token.token)}, status=status.HTTP_200_OK)
else:
# Check the user type
user_type = 1 if request.user.is_bot else 0
@@ -114,9 +92,5 @@ class ServiceApiTokenEndpoint(BaseAPIView):
is_service=True,
)
return Response(
{
"token": str(api_token.token),
},
status=status.HTTP_201_CREATED,
{"token": str(api_token.token)}, status=status.HTTP_201_CREATED
)
+9 -21
View File
@@ -10,11 +10,7 @@ from plane.app.serializers import FileAssetSerializer
class FileAssetEndpoint(BaseAPIView):
parser_classes = (
MultiPartParser,
FormParser,
JSONParser,
)
parser_classes = (MultiPartParser, FormParser, JSONParser)
"""
A viewset for viewing and editing task instances.
@@ -28,8 +24,7 @@ class FileAssetEndpoint(BaseAPIView):
files, context={"request": request}, many=True
)
return Response(
{"data": serializer.data, "status": True},
status=status.HTTP_200_OK,
{"data": serializer.data, "status": True}, status=status.HTTP_200_OK
)
else:
return Response(
@@ -50,7 +45,7 @@ class FileAssetEndpoint(BaseAPIView):
asset_key = str(workspace_id) + "/" + asset_key
file_asset = FileAsset.objects.get(asset=asset_key)
file_asset.is_deleted = True
file_asset.save()
file_asset.save(update_fields=["is_deleted"])
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -59,7 +54,7 @@ class FileAssetViewSet(BaseViewSet):
asset_key = str(workspace_id) + "/" + asset_key
file_asset = FileAsset.objects.get(asset=asset_key)
file_asset.is_deleted = False
file_asset.save()
file_asset.save(update_fields=["is_deleted"])
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -67,16 +62,11 @@ class UserAssetsEndpoint(BaseAPIView):
parser_classes = (MultiPartParser, FormParser)
def get(self, request, asset_key):
files = FileAsset.objects.filter(
asset=asset_key, created_by=request.user
)
files = FileAsset.objects.filter(asset=asset_key, created_by=request.user)
if files.exists():
serializer = FileAssetSerializer(
files, context={"request": request}
)
serializer = FileAssetSerializer(files, context={"request": request})
return Response(
{"data": serializer.data, "status": True},
status=status.HTTP_200_OK,
{"data": serializer.data, "status": True}, status=status.HTTP_200_OK
)
else:
return Response(
@@ -92,9 +82,7 @@ class UserAssetsEndpoint(BaseAPIView):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, asset_key):
file_asset = FileAsset.objects.get(
asset=asset_key, created_by=request.user
)
file_asset = FileAsset.objects.get(asset=asset_key, created_by=request.user)
file_asset.is_deleted = True
file_asset.save()
file_asset.save(update_fields=["is_deleted"])
return Response(status=status.HTTP_204_NO_CONTENT)
+83 -199
View File
@@ -13,12 +13,7 @@ from rest_framework.permissions import AllowAny
# Module imports
from ..base import BaseAPIView
from plane.db.models import (
FileAsset,
Workspace,
Project,
User,
)
from plane.db.models import FileAsset, Workspace, Project, User
from plane.settings.storage import S3Storage
from plane.app.permissions import allow_permission, ROLE
from plane.utils.cache import invalidate_cache_directly
@@ -34,7 +29,7 @@ class UserAssetsV2Endpoint(BaseAPIView):
return
asset.is_deleted = True
asset.deleted_at = timezone.now()
asset.save()
asset.save(update_fields=["is_deleted", "deleted_at"])
return
def entity_asset_save(self, asset_id, entity_type, asset, request):
@@ -49,10 +44,7 @@ class UserAssetsV2Endpoint(BaseAPIView):
user.avatar_asset_id = asset_id
user.save()
invalidate_cache_directly(
path="/api/users/me/",
url_params=False,
user=True,
request=request,
path="/api/users/me/", url_params=False, user=True, request=request
)
invalidate_cache_directly(
path="/api/users/me/settings/",
@@ -72,10 +64,7 @@ class UserAssetsV2Endpoint(BaseAPIView):
user.cover_image_asset_id = asset_id
user.save()
invalidate_cache_directly(
path="/api/users/me/",
url_params=False,
user=True,
request=request,
path="/api/users/me/", url_params=False, user=True, request=request
)
invalidate_cache_directly(
path="/api/users/me/settings/",
@@ -93,10 +82,7 @@ class UserAssetsV2Endpoint(BaseAPIView):
user.avatar_asset_id = None
user.save()
invalidate_cache_directly(
path="/api/users/me/",
url_params=False,
user=True,
request=request,
path="/api/users/me/", url_params=False, user=True, request=request
)
invalidate_cache_directly(
path="/api/users/me/settings/",
@@ -111,10 +97,7 @@ class UserAssetsV2Endpoint(BaseAPIView):
user.cover_image_asset_id = None
user.save()
invalidate_cache_directly(
path="/api/users/me/",
url_params=False,
user=True,
request=request,
path="/api/users/me/", url_params=False, user=True, request=request
)
invalidate_cache_directly(
path="/api/users/me/settings/",
@@ -138,10 +121,7 @@ class UserAssetsV2Endpoint(BaseAPIView):
# Check if the entity type is allowed
if not entity_type or entity_type not in ["USER_AVATAR", "USER_COVER"]:
return Response(
{
"error": "Invalid entity type.",
"status": False,
},
{"error": "Invalid entity type.", "status": False},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -161,11 +141,7 @@ class UserAssetsV2Endpoint(BaseAPIView):
# Create a File Asset
asset = FileAsset.objects.create(
attributes={
"name": name,
"type": type,
"size": size_limit,
},
attributes={"name": name, "type": type, "size": size_limit},
asset=asset_key,
size=size_limit,
user=request.user,
@@ -177,9 +153,7 @@ class UserAssetsV2Endpoint(BaseAPIView):
storage = S3Storage(request=request)
# Generate a presigned URL to share an S3 object
presigned_url = storage.generate_presigned_post(
object_name=asset_key,
file_type=type,
file_size=size_limit,
object_name=asset_key, file_type=type, file_size=size_limit
)
# Return the presigned URL
return Response(
@@ -209,7 +183,7 @@ class UserAssetsV2Endpoint(BaseAPIView):
# update the attributes
asset.attributes = request.data.get("attributes", asset.attributes)
# save the asset
asset.save()
asset.save(update_fields=["is_uploaded", "attributes"])
return Response(status=status.HTTP_204_NO_CONTENT)
def delete(self, request, asset_id):
@@ -220,7 +194,7 @@ class UserAssetsV2Endpoint(BaseAPIView):
self.entity_asset_delete(
entity_type=asset.entity_type, asset=asset, request=request
)
asset.save()
asset.save(update_fields=["is_deleted", "deleted_at"])
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -230,45 +204,33 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
def get_entity_id_field(self, entity_type, entity_id):
# Workspace Logo
if entity_type == FileAsset.EntityTypeContext.WORKSPACE_LOGO:
return {
"workspace_id": entity_id,
}
return {"workspace_id": entity_id}
# Project Cover
if entity_type == FileAsset.EntityTypeContext.PROJECT_COVER:
return {
"project_id": entity_id,
}
return {"project_id": entity_id}
# User Avatar and Cover
if entity_type in [
FileAsset.EntityTypeContext.USER_AVATAR,
FileAsset.EntityTypeContext.USER_COVER,
]:
return {
"user_id": entity_id,
}
return {"user_id": entity_id}
# Issue Attachment and Description
if entity_type in [
FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
FileAsset.EntityTypeContext.ISSUE_DESCRIPTION,
]:
return {
"issue_id": entity_id,
}
return {"issue_id": entity_id}
# Page Description
if entity_type == FileAsset.EntityTypeContext.PAGE_DESCRIPTION:
return {
"page_id": entity_id,
}
return {"page_id": entity_id}
# Comment Description
if entity_type == FileAsset.EntityTypeContext.COMMENT_DESCRIPTION:
return {
"comment_id": entity_id,
}
return {"comment_id": entity_id}
return {}
def asset_delete(self, asset_id):
@@ -279,7 +241,7 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
# Mark the asset as deleted
asset.is_deleted = True
asset.deleted_at = timezone.now()
asset.save()
asset.save(update_fields=["is_deleted", "deleted_at"])
return
def entity_asset_save(self, asset_id, entity_type, asset, request):
@@ -296,10 +258,7 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
workspace.logo_asset_id = asset_id
workspace.save()
invalidate_cache_directly(
path="/api/workspaces/",
url_params=False,
user=False,
request=request,
path="/api/workspaces/", url_params=False, user=False, request=request
)
invalidate_cache_directly(
path="/api/users/me/workspaces/",
@@ -308,16 +267,13 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
request=request,
)
invalidate_cache_directly(
path="/api/instances/",
url_params=False,
user=False,
request=request,
path="/api/instances/", url_params=False, user=False, request=request
)
return
# Project Cover
elif entity_type == FileAsset.EntityTypeContext.PROJECT_COVER:
project = Project.objects.filter(id=asset.workspace_id).first()
project = Project.objects.filter(id=asset.project_id).first()
if project is None:
return
# Delete the previous cover image
@@ -340,10 +296,7 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
workspace.logo_asset_id = None
workspace.save()
invalidate_cache_directly(
path="/api/workspaces/",
url_params=False,
user=False,
request=request,
path="/api/workspaces/", url_params=False, user=False, request=request
)
invalidate_cache_directly(
path="/api/users/me/workspaces/",
@@ -352,10 +305,7 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
request=request,
)
invalidate_cache_directly(
path="/api/instances/",
url_params=False,
user=False,
request=request,
path="/api/instances/", url_params=False, user=False, request=request
)
return
# Project Cover
@@ -379,15 +329,18 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
# Check if the entity type is allowed
if entity_type not in FileAsset.EntityTypeContext.values:
return Response(
{
"error": "Invalid entity type.",
"status": False,
},
{"error": "Invalid entity type.", "status": False},
status=status.HTTP_400_BAD_REQUEST,
)
# Check if the file type is allowed
allowed_types = ["image/jpeg", "image/png", "image/webp", "image/jpg"]
allowed_types = [
"image/jpeg",
"image/png",
"image/webp",
"image/jpg",
"image/gif",
]
if type not in allowed_types:
return Response(
{
@@ -408,11 +361,7 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
# Create a File Asset
asset = FileAsset.objects.create(
attributes={
"name": name,
"type": type,
"size": size_limit,
},
attributes={"name": name, "type": type, "size": size_limit},
asset=asset_key,
size=size_limit,
workspace=workspace,
@@ -427,9 +376,7 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
storage = S3Storage(request=request)
# Generate a presigned URL to share an S3 object
presigned_url = storage.generate_presigned_post(
object_name=asset_key,
file_type=type,
file_size=size_limit,
object_name=asset_key, file_type=type, file_size=size_limit
)
# Return the presigned URL
return Response(
@@ -459,7 +406,7 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
# update the attributes
asset.attributes = request.data.get("attributes", asset.attributes)
# save the asset
asset.save()
asset.save(update_fields=["is_uploaded", "attributes"])
return Response(status=status.HTTP_204_NO_CONTENT)
def delete(self, request, slug, asset_id):
@@ -470,7 +417,7 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
self.entity_asset_delete(
entity_type=asset.entity_type, asset=asset, request=request
)
asset.save()
asset.save(update_fields=["is_deleted", "deleted_at"])
return Response(status=status.HTTP_204_NO_CONTENT)
def get(self, request, slug, asset_id):
@@ -480,18 +427,14 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
# Check if the asset is uploaded
if not asset.is_uploaded:
return Response(
{
"error": "The requested asset could not be found.",
},
{"error": "The requested asset could not be found."},
status=status.HTTP_404_NOT_FOUND,
)
# Get the presigned URL
storage = S3Storage(request=request)
# Generate a presigned URL to share an S3 object
signed_url = storage.generate_presigned_url(
object_name=asset.asset.name,
)
signed_url = storage.generate_presigned_url(object_name=asset.asset.name)
# Redirect to the signed URL
return HttpResponseRedirect(signed_url)
@@ -499,9 +442,7 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
class StaticFileAssetEndpoint(BaseAPIView):
"""This endpoint is used to get the signed URL for a static asset."""
permission_classes = [
AllowAny,
]
permission_classes = [AllowAny]
def get(self, request, asset_id):
# get the asset id
@@ -510,9 +451,7 @@ class StaticFileAssetEndpoint(BaseAPIView):
# Check if the asset is uploaded
if not asset.is_uploaded:
return Response(
{
"error": "The requested asset could not be found.",
},
{"error": "The requested asset could not be found."},
status=status.HTTP_404_NOT_FOUND,
)
@@ -524,19 +463,14 @@ class StaticFileAssetEndpoint(BaseAPIView):
FileAsset.EntityTypeContext.PROJECT_COVER,
]:
return Response(
{
"error": "Invalid entity type.",
"status": False,
},
{"error": "Invalid entity type.", "status": False},
status=status.HTTP_400_BAD_REQUEST,
)
# Get the presigned URL
storage = S3Storage(request=request)
# Generate a presigned URL to share an S3 object
signed_url = storage.generate_presigned_url(
object_name=asset.asset.name,
)
signed_url = storage.generate_presigned_url(object_name=asset.asset.name)
# Redirect to the signed URL
return HttpResponseRedirect(signed_url)
@@ -549,7 +483,7 @@ class AssetRestoreEndpoint(BaseAPIView):
asset = FileAsset.all_objects.get(id=asset_id, workspace__slug=slug)
asset.is_deleted = False
asset.deleted_at = None
asset.save()
asset.save(update_fields=["is_deleted", "deleted_at"])
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -558,50 +492,34 @@ class ProjectAssetEndpoint(BaseAPIView):
def get_entity_id_field(self, entity_type, entity_id):
if entity_type == FileAsset.EntityTypeContext.WORKSPACE_LOGO:
return {
"workspace_id": entity_id,
}
return {"workspace_id": entity_id}
if entity_type == FileAsset.EntityTypeContext.PROJECT_COVER:
return {
"project_id": entity_id,
}
return {"project_id": entity_id}
if entity_type in [
FileAsset.EntityTypeContext.USER_AVATAR,
FileAsset.EntityTypeContext.USER_COVER,
]:
return {
"user_id": entity_id,
}
return {"user_id": entity_id}
if entity_type in [
FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
FileAsset.EntityTypeContext.ISSUE_DESCRIPTION,
]:
return {
"issue_id": entity_id,
}
return {"issue_id": entity_id}
if entity_type == FileAsset.EntityTypeContext.PAGE_DESCRIPTION:
return {
"page_id": entity_id,
}
return {"page_id": entity_id}
if entity_type == FileAsset.EntityTypeContext.COMMENT_DESCRIPTION:
return {
"comment_id": entity_id,
}
return {"comment_id": entity_id}
if entity_type == FileAsset.EntityTypeContext.DRAFT_ISSUE_DESCRIPTION:
return {
"draft_issue_id": entity_id,
}
return {"draft_issue_id": entity_id}
return {}
@allow_permission(
[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST],
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def post(self, request, slug, project_id):
name = request.data.get("name")
type = request.data.get("type", "image/jpeg")
@@ -612,15 +530,18 @@ class ProjectAssetEndpoint(BaseAPIView):
# Check if the entity type is allowed
if entity_type not in FileAsset.EntityTypeContext.values:
return Response(
{
"error": "Invalid entity type.",
"status": False,
},
{"error": "Invalid entity type.", "status": False},
status=status.HTTP_400_BAD_REQUEST,
)
# Check if the file type is allowed
allowed_types = ["image/jpeg", "image/png", "image/webp", "image/jpg"]
allowed_types = [
"image/jpeg",
"image/png",
"image/webp",
"image/jpg",
"image/gif",
]
if type not in allowed_types:
return Response(
{
@@ -641,11 +562,7 @@ class ProjectAssetEndpoint(BaseAPIView):
# Create a File Asset
asset = FileAsset.objects.create(
attributes={
"name": name,
"type": type,
"size": size_limit,
},
attributes={"name": name, "type": type, "size": size_limit},
asset=asset_key,
size=size_limit,
workspace=workspace,
@@ -659,9 +576,7 @@ class ProjectAssetEndpoint(BaseAPIView):
storage = S3Storage(request=request)
# Generate a presigned URL to share an S3 object
presigned_url = storage.generate_presigned_post(
object_name=asset_key,
file_type=type,
file_size=size_limit,
object_name=asset_key, file_type=type, file_size=size_limit
)
# Return the presigned URL
return Response(
@@ -673,14 +588,10 @@ class ProjectAssetEndpoint(BaseAPIView):
status=status.HTTP_200_OK,
)
@allow_permission(
[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST],
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def patch(self, request, slug, project_id, pk):
# get the asset id
asset = FileAsset.objects.get(
id=pk,
)
asset = FileAsset.objects.get(id=pk)
# get the storage metadata
asset.is_uploaded = True
# get the storage metadata
@@ -690,53 +601,49 @@ class ProjectAssetEndpoint(BaseAPIView):
# update the attributes
asset.attributes = request.data.get("attributes", asset.attributes)
# save the asset
asset.save()
asset.save(update_fields=["is_uploaded", "attributes"])
return Response(status=status.HTTP_204_NO_CONTENT)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def delete(self, request, slug, project_id, pk):
# Get the asset
asset = FileAsset.objects.get(
id=pk,
workspace__slug=slug,
project_id=project_id,
id=pk, workspace__slug=slug, project_id=project_id
)
# Check deleted assets
asset.is_deleted = True
asset.deleted_at = timezone.now()
# Save the asset
asset.save()
asset.save(update_fields=["is_deleted", "deleted_at"])
return Response(status=status.HTTP_204_NO_CONTENT)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def get(self, request, slug, project_id, pk):
# get the asset id
asset = FileAsset.objects.get(
workspace__slug=slug,
project_id=project_id,
pk=pk,
workspace__slug=slug, project_id=project_id, pk=pk
)
# Check if the asset is uploaded
if not asset.is_uploaded:
return Response(
{
"error": "The requested asset could not be found.",
},
{"error": "The requested asset could not be found."},
status=status.HTTP_404_NOT_FOUND,
)
# Get the presigned URL
storage = S3Storage(request=request)
# Generate a presigned URL to share an S3 object
signed_url = storage.generate_presigned_url(
object_name=asset.asset.name,
)
signed_url = storage.generate_presigned_url(object_name=asset.asset.name)
# Redirect to the signed URL
return HttpResponseRedirect(signed_url)
class ProjectBulkAssetEndpoint(BaseAPIView):
def save_project_cover(self, asset, project_id):
project = Project.objects.get(id=project_id)
project.cover_image_asset_id = asset.id
project.save()
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def post(self, request, slug, project_id, entity_id):
@@ -745,59 +652,36 @@ class ProjectBulkAssetEndpoint(BaseAPIView):
# Check if the asset ids are provided
if not asset_ids:
return Response(
{
"error": "No asset ids provided.",
},
status=status.HTTP_400_BAD_REQUEST,
{"error": "No asset ids provided."}, status=status.HTTP_400_BAD_REQUEST
)
# get the asset id
assets = FileAsset.objects.filter(
id__in=asset_ids,
workspace__slug=slug,
)
assets = FileAsset.objects.filter(id__in=asset_ids, workspace__slug=slug)
# Get the first asset
asset = assets.first()
if not asset:
return Response(
{
"error": "The requested asset could not be found.",
},
{"error": "The requested asset could not be found."},
status=status.HTTP_404_NOT_FOUND,
)
# Check if the asset is uploaded
if asset.entity_type == FileAsset.EntityTypeContext.PROJECT_COVER:
assets.update(
project_id=project_id,
)
assets.update(project_id=project_id)
[self.save_project_cover(asset, project_id) for asset in assets]
if asset.entity_type == FileAsset.EntityTypeContext.ISSUE_DESCRIPTION:
assets.update(
issue_id=entity_id,
)
assets.update(issue_id=entity_id)
if (
asset.entity_type
== FileAsset.EntityTypeContext.COMMENT_DESCRIPTION
):
assets.update(
comment_id=entity_id,
)
if asset.entity_type == FileAsset.EntityTypeContext.COMMENT_DESCRIPTION:
assets.update(comment_id=entity_id)
if asset.entity_type == FileAsset.EntityTypeContext.PAGE_DESCRIPTION:
assets.update(
page_id=entity_id,
)
assets.update(page_id=entity_id)
if (
asset.entity_type
== FileAsset.EntityTypeContext.DRAFT_ISSUE_DESCRIPTION
):
assets.update(
draft_issue_id=entity_id,
)
if asset.entity_type == FileAsset.EntityTypeContext.DRAFT_ISSUE_DESCRIPTION:
assets.update(draft_issue_id=entity_id)
return Response(status=status.HTTP_204_NO_CONTENT)
+11 -35
View File
@@ -43,18 +43,11 @@ class TimezoneMixin:
class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
model = None
permission_classes = [
IsAuthenticated,
]
permission_classes = [IsAuthenticated]
filter_backends = (
DjangoFilterBackend,
SearchFilter,
)
filter_backends = (DjangoFilterBackend, SearchFilter)
authentication_classes = [
BaseSessionAuthentication,
]
authentication_classes = [BaseSessionAuthentication]
filterset_fields = []
@@ -65,9 +58,7 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
return self.model.objects.all()
except Exception as e:
log_exception(e)
raise APIException(
"Please check the view", status.HTTP_400_BAD_REQUEST
)
raise APIException("Please check the view", status.HTTP_400_BAD_REQUEST)
def handle_exception(self, exc):
"""
@@ -146,35 +137,24 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
@property
def fields(self):
fields = [
field
for field in self.request.GET.get("fields", "").split(",")
if field
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
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 = [
IsAuthenticated,
]
permission_classes = [IsAuthenticated]
filter_backends = (
DjangoFilterBackend,
SearchFilter,
)
filter_backends = (DjangoFilterBackend, SearchFilter)
authentication_classes = [
BaseSessionAuthentication,
]
authentication_classes = [BaseSessionAuthentication]
filterset_fields = []
@@ -251,17 +231,13 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
@property
def fields(self):
fields = [
field
for field in self.request.GET.get("fields", "").split(",")
if field
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
expand for expand in self.request.GET.get("expand", "").split(",") if expand
]
return expand if expand else None
+40 -68
View File
@@ -34,7 +34,6 @@ from .. import BaseAPIView
class CycleArchiveUnarchiveEndpoint(BaseAPIView):
def get_queryset(self):
favorite_subquery = UserFavorite.objects.filter(
user=self.request.user,
@@ -48,12 +47,11 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
estimate_point__estimate__type="points",
state__group="backlog",
issue_cycle__cycle_id=OuterRef("pk"),
issue_cycle__deleted_at__isnull=True,
)
.values("issue_cycle__cycle_id")
.annotate(
backlog_estimate_point=Sum(
Cast("estimate_point__value", FloatField())
)
backlog_estimate_point=Sum(Cast("estimate_point__value", FloatField()))
)
.values("backlog_estimate_point")[:1]
)
@@ -62,6 +60,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
estimate_point__estimate__type="points",
state__group="unstarted",
issue_cycle__cycle_id=OuterRef("pk"),
issue_cycle__deleted_at__isnull=True,
)
.values("issue_cycle__cycle_id")
.annotate(
@@ -76,12 +75,11 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
estimate_point__estimate__type="points",
state__group="started",
issue_cycle__cycle_id=OuterRef("pk"),
issue_cycle__deleted_at__isnull=True,
)
.values("issue_cycle__cycle_id")
.annotate(
started_estimate_point=Sum(
Cast("estimate_point__value", FloatField())
)
started_estimate_point=Sum(Cast("estimate_point__value", FloatField()))
)
.values("started_estimate_point")[:1]
)
@@ -90,6 +88,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
estimate_point__estimate__type="points",
state__group="cancelled",
issue_cycle__cycle_id=OuterRef("pk"),
issue_cycle__deleted_at__isnull=True,
)
.values("issue_cycle__cycle_id")
.annotate(
@@ -104,6 +103,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
estimate_point__estimate__type="points",
state__group="completed",
issue_cycle__cycle_id=OuterRef("pk"),
issue_cycle__deleted_at__isnull=True,
)
.values("issue_cycle__cycle_id")
.annotate(
@@ -117,12 +117,11 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
Issue.issue_objects.filter(
estimate_point__estimate__type="points",
issue_cycle__cycle_id=OuterRef("pk"),
issue_cycle__deleted_at__isnull=True,
)
.values("issue_cycle__cycle_id")
.annotate(
total_estimate_points=Sum(
Cast("estimate_point__value", FloatField())
)
total_estimate_points=Sum(Cast("estimate_point__value", FloatField()))
)
.values("total_estimate_points")[:1]
)
@@ -147,9 +146,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
.prefetch_related(
Prefetch(
"issue_cycle__issue__labels",
queryset=Label.objects.only(
"name", "color", "id"
).distinct(),
queryset=Label.objects.only("name", "color", "id").distinct(),
)
)
.annotate(is_favorite=Exists(favorite_subquery))
@@ -231,9 +228,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
& Q(end_date__gte=timezone.now()),
then=Value("CURRENT"),
),
When(
start_date__gt=timezone.now(), then=Value("UPCOMING")
),
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),
@@ -248,9 +243,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
ArrayAgg(
"issue_cycle__issue__assignees__id",
distinct=True,
filter=~Q(
issue_cycle__issue__assignees__id__isnull=True
),
filter=~Q(issue_cycle__issue__assignees__id__isnull=True),
),
Value([], output_field=ArrayField(UUIDField())),
)
@@ -259,48 +252,42 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
backlog_estimate_points=Coalesce(
Subquery(backlog_estimate_point),
Value(0, output_field=FloatField()),
),
)
)
.annotate(
unstarted_estimate_points=Coalesce(
Subquery(unstarted_estimate_point),
Value(0, output_field=FloatField()),
),
)
)
.annotate(
started_estimate_points=Coalesce(
Subquery(started_estimate_point),
Value(0, output_field=FloatField()),
),
)
)
.annotate(
cancelled_estimate_points=Coalesce(
Subquery(cancelled_estimate_point),
Value(0, output_field=FloatField()),
),
)
)
.annotate(
completed_estimate_points=Coalesce(
Subquery(completed_estimate_point),
Value(0, output_field=FloatField()),
),
)
)
.annotate(
total_estimate_points=Coalesce(
Subquery(total_estimate_point),
Value(0, output_field=FloatField()),
),
Subquery(total_estimate_point), Value(0, output_field=FloatField())
)
)
.order_by("-is_favorite", "name")
.distinct()
)
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
]
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def get(self, request, slug, project_id, pk=None):
if pk is None:
queryset = (
@@ -336,9 +323,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
return Response(queryset, status=status.HTTP_200_OK)
else:
queryset = (
self.get_queryset()
.filter(archived_at__isnull=False)
.filter(pk=pk)
self.get_queryset().filter(archived_at__isnull=False).filter(pk=pk)
)
data = (
self.get_queryset()
@@ -348,6 +333,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
project_id=self.kwargs.get("project_id"),
parent__isnull=False,
issue_cycle__cycle_id=pk,
issue_cycle__deleted_at__isnull=True,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
@@ -402,6 +388,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
assignee_distribution = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=pk,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
@@ -429,9 +416,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
)
.values("display_name", "assignee_id", "avatar_url")
.annotate(
total_estimates=Sum(
Cast("estimate_point__value", FloatField())
)
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
)
.annotate(
completed_estimates=Sum(
@@ -459,6 +444,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
label_distribution = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=pk,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
@@ -467,9 +453,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
.annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id")
.annotate(
total_estimates=Sum(
Cast("estimate_point__value", FloatField())
)
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
)
.annotate(
completed_estimates=Sum(
@@ -500,20 +484,19 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
}
if data["start_date"] and data["end_date"]:
data["estimate_distribution"]["completion_chart"] = (
burndown_plot(
queryset=queryset,
slug=slug,
project_id=project_id,
plot_type="points",
cycle_id=pk,
)
data["estimate_distribution"]["completion_chart"] = burndown_plot(
queryset=queryset,
slug=slug,
project_id=project_id,
plot_type="points",
cycle_id=pk,
)
# Assignee Distribution
assignee_distribution = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=pk,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
@@ -550,12 +533,8 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
)
.annotate(
total_issues=Count(
"id",
filter=Q(
archived_at__isnull=True,
is_draft=False,
),
),
"id", filter=Q(archived_at__isnull=True, is_draft=False)
)
)
.annotate(
completed_issues=Count(
@@ -584,6 +563,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
label_distribution = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=pk,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
@@ -593,12 +573,8 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
.values("label_name", "color", "label_id")
.annotate(
total_issues=Count(
"id",
filter=Q(
archived_at__isnull=True,
is_draft=False,
),
),
"id", filter=Q(archived_at__isnull=True, is_draft=False)
)
)
.annotate(
completed_issues=Count(
@@ -638,10 +614,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
cycle_id=pk,
)
return Response(
data,
status=status.HTTP_200_OK,
)
return Response(data, status=status.HTTP_200_OK)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def post(self, request, slug, project_id, cycle_id):
@@ -664,8 +637,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
workspace__slug=slug,
).delete()
return Response(
{"archived_at": str(cycle.archived_at)},
status=status.HTTP_200_OK,
{"archived_at": str(cycle.archived_at)}, status=status.HTTP_200_OK
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
+68 -111
View File
@@ -89,9 +89,7 @@ class CycleViewSet(BaseViewSet):
.prefetch_related(
Prefetch(
"issue_cycle__issue__labels",
queryset=Label.objects.only(
"name", "color", "id"
).distinct(),
queryset=Label.objects.only("name", "color", "id").distinct(),
)
)
.annotate(is_favorite=Exists(favorite_subquery))
@@ -102,7 +100,7 @@ class CycleViewSet(BaseViewSet):
filter=Q(
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -114,7 +112,7 @@ class CycleViewSet(BaseViewSet):
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -125,9 +123,7 @@ class CycleViewSet(BaseViewSet):
& Q(end_date__gte=timezone.now()),
then=Value("CURRENT"),
),
When(
start_date__gt=timezone.now(), then=Value("UPCOMING")
),
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),
@@ -142,8 +138,11 @@ class CycleViewSet(BaseViewSet):
ArrayAgg(
"issue_cycle__issue__assignees__id",
distinct=True,
filter=~Q(
issue_cycle__issue__assignees__id__isnull=True
filter=~Q(issue_cycle__issue__assignees__id__isnull=True)
& (
Q(
issue_cycle__issue__issue_assignee__deleted_at__isnull=True
)
),
),
Value([], output_field=ArrayField(UUIDField())),
@@ -164,8 +163,7 @@ class CycleViewSet(BaseViewSet):
# Current Cycle
if cycle_view == "current":
queryset = queryset.filter(
start_date__lte=timezone.now(),
end_date__gte=timezone.now(),
start_date__lte=timezone.now(), end_date__gte=timezone.now()
)
data = queryset.values(
@@ -236,10 +234,7 @@ class CycleViewSet(BaseViewSet):
):
serializer = CycleWriteSerializer(data=request.data)
if serializer.is_valid():
serializer.save(
project_id=project_id,
owned_by=request.user,
)
serializer.save(project_id=project_id, owned_by=request.user)
cycle = (
self.get_queryset()
.filter(pk=serializer.data["id"])
@@ -283,9 +278,7 @@ class CycleViewSet(BaseViewSet):
origin=request.META.get("HTTP_ORIGIN"),
)
return Response(cycle, status=status.HTTP_201_CREATED)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else:
return Response(
{
@@ -316,9 +309,7 @@ class CycleViewSet(BaseViewSet):
if "sort_order" in request_data:
# Can only change sort order for a completed cycle``
request_data = {
"sort_order": request_data.get(
"sort_order", cycle.sort_order
)
"sort_order": request_data.get("sort_order", cycle.sort_order)
}
else:
return Response(
@@ -328,9 +319,7 @@ class CycleViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
serializer = CycleWriteSerializer(
cycle, data=request.data, partial=True
)
serializer = CycleWriteSerializer(cycle, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
cycle = queryset.values(
@@ -374,16 +363,9 @@ class CycleViewSet(BaseViewSet):
return Response(cycle, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
]
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def retrieve(self, request, slug, project_id, pk):
queryset = (
self.get_queryset().filter(archived_at__isnull=True).filter(pk=pk)
)
queryset = self.get_queryset().filter(archived_at__isnull=True).filter(pk=pk)
data = (
self.get_queryset()
.filter(pk=pk)
@@ -393,6 +375,7 @@ class CycleViewSet(BaseViewSet):
project_id=self.kwargs.get("project_id"),
parent__isnull=False,
issue_cycle__cycle_id=pk,
issue_cycle__deleted_at__isnull=True,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
@@ -430,8 +413,7 @@ class CycleViewSet(BaseViewSet):
if data is None:
return Response(
{"error": "Cycle not found"},
status=status.HTTP_404_NOT_FOUND,
{"error": "Cycle not found"}, status=status.HTTP_404_NOT_FOUND
)
queryset = queryset.first()
@@ -443,16 +425,11 @@ class CycleViewSet(BaseViewSet):
user_id=request.user.id,
project_id=project_id,
)
return Response(
data,
status=status.HTTP_200_OK,
)
return Response(data, status=status.HTTP_200_OK)
@allow_permission([ROLE.ADMIN], creator=True, model=Cycle)
def destroy(self, request, slug, project_id, pk):
cycle = Cycle.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
if cycle.owned_by_id != request.user.id and not (
ProjectMember.objects.filter(
workspace__slug=slug,
@@ -468,9 +445,9 @@ class CycleViewSet(BaseViewSet):
)
cycle_issues = list(
CycleIssue.objects.filter(
cycle_id=self.kwargs.get("pk")
).values_list("issue", flat=True)
CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
"issue", flat=True
)
)
issue_activity.delay(
@@ -491,7 +468,7 @@ class CycleViewSet(BaseViewSet):
origin=request.META.get("HTTP_ORIGIN"),
)
# TODO: Soft delete the cycle break the onetoone relationship with cycle issue
cycle.delete(soft=False)
cycle.delete()
# Delete the user favorite cycle
UserFavorite.objects.filter(
@@ -596,6 +573,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
filter=Q(
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -607,6 +585,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -618,6 +597,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -629,6 +609,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -640,6 +621,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -651,6 +633,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -667,6 +650,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
assignee_estimate_data = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
@@ -694,9 +678,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
)
.values("display_name", "assignee_id", "avatar_url")
.annotate(
total_estimates=Sum(
Cast("estimate_point__value", FloatField())
)
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
)
.annotate(
completed_estimates=Sum(
@@ -725,9 +707,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
{
"display_name": item["display_name"],
"assignee_id": (
str(item["assignee_id"])
if item["assignee_id"]
else None
str(item["assignee_id"]) if item["assignee_id"] else None
),
"avatar": item.get("avatar"),
"avatar_url": item.get("avatar_url"),
@@ -741,6 +721,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
label_distribution_data = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
@@ -749,9 +730,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
.annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id")
.annotate(
total_estimates=Sum(
Cast("estimate_point__value", FloatField())
)
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
)
.annotate(
completed_estimates=Sum(
@@ -788,9 +767,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
{
"label_name": item["label_name"],
"color": item["color"],
"label_id": (
str(item["label_id"]) if item["label_id"] else None
),
"label_id": (str(item["label_id"]) if item["label_id"] else None),
"total_estimates": item["total_estimates"],
"completed_estimates": item["completed_estimates"],
"pending_estimates": item["pending_estimates"],
@@ -802,6 +779,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
assignee_distribution = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
@@ -820,8 +798,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
),
# If `avatar_asset` is None, fall back to using `avatar` field directly
When(
assignees__avatar_asset__isnull=True,
then="assignees__avatar",
assignees__avatar_asset__isnull=True, then="assignees__avatar"
),
default=Value(None),
output_field=models.CharField(),
@@ -830,12 +807,8 @@ class TransferCycleIssueEndpoint(BaseAPIView):
.values("display_name", "assignee_id", "avatar_url")
.annotate(
total_issues=Count(
"id",
filter=Q(
archived_at__isnull=True,
is_draft=False,
),
),
"id", filter=Q(archived_at__isnull=True, is_draft=False)
)
)
.annotate(
completed_issues=Count(
@@ -879,6 +852,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
label_distribution = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
@@ -888,12 +862,8 @@ class TransferCycleIssueEndpoint(BaseAPIView):
.values("label_name", "color", "label_id")
.annotate(
total_issues=Count(
"id",
filter=Q(
archived_at__isnull=True,
is_draft=False,
),
),
"id", filter=Q(archived_at__isnull=True, is_draft=False)
)
)
.annotate(
completed_issues=Count(
@@ -923,9 +893,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
{
"label_name": item["label_name"],
"color": item["color"],
"label_id": (
str(item["label_id"]) if item["label_id"] else None
),
"label_id": (str(item["label_id"]) if item["label_id"] else None),
"total_issues": item["total_issues"],
"completed_issues": item["completed_issues"],
"pending_issues": item["pending_issues"],
@@ -970,10 +938,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
}
current_cycle.save(update_fields=["progress_snapshot"])
if (
new_cycle.end_date is not None
and new_cycle.end_date < timezone.now()
):
if new_cycle.end_date is not None and new_cycle.end_date < timezone.now():
return Response(
{
"error": "The cycle where the issues are transferred is already completed"
@@ -1036,9 +1001,7 @@ class CycleUserPropertiesEndpoint(BaseAPIView):
workspace__slug=slug,
)
cycle_properties.filters = request.data.get(
"filters", cycle_properties.filters
)
cycle_properties.filters = request.data.get("filters", cycle_properties.filters)
cycle_properties.display_filters = request.data.get(
"display_filters", cycle_properties.display_filters
)
@@ -1069,12 +1032,11 @@ class CycleProgressEndpoint(BaseAPIView):
Issue.issue_objects.filter(
estimate_point__estimate__type="points",
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
.annotate(
value_as_float=Cast("estimate_point__value", FloatField())
)
.annotate(value_as_float=Cast("estimate_point__value", FloatField()))
.aggregate(
backlog_estimate_point=Sum(
Case(
@@ -1112,15 +1074,14 @@ class CycleProgressEndpoint(BaseAPIView):
)
),
total_estimate_points=Sum(
"value_as_float",
default=Value(0),
output_field=FloatField(),
"value_as_float", default=Value(0), output_field=FloatField()
),
)
)
backlog_issues = Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
state__group="backlog",
@@ -1128,6 +1089,7 @@ class CycleProgressEndpoint(BaseAPIView):
unstarted_issues = Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
state__group="unstarted",
@@ -1135,6 +1097,7 @@ class CycleProgressEndpoint(BaseAPIView):
started_issues = Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
state__group="started",
@@ -1142,6 +1105,7 @@ class CycleProgressEndpoint(BaseAPIView):
cancelled_issues = Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
state__group="cancelled",
@@ -1149,6 +1113,7 @@ class CycleProgressEndpoint(BaseAPIView):
completed_issues = Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
state__group="completed",
@@ -1156,23 +1121,20 @@ class CycleProgressEndpoint(BaseAPIView):
total_issues = Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
).count()
return Response(
{
"backlog_estimate_points": aggregate_estimates[
"backlog_estimate_point"
]
"backlog_estimate_points": aggregate_estimates["backlog_estimate_point"]
or 0,
"unstarted_estimate_points": aggregate_estimates[
"unstarted_estimate_point"
]
or 0,
"started_estimate_points": aggregate_estimates[
"started_estimate_point"
]
"started_estimate_points": aggregate_estimates["started_estimate_point"]
or 0,
"cancelled_estimate_points": aggregate_estimates[
"cancelled_estimate_point"
@@ -1182,9 +1144,7 @@ class CycleProgressEndpoint(BaseAPIView):
"completed_estimate_points"
]
or 0,
"total_estimate_points": aggregate_estimates[
"total_estimate_points"
],
"total_estimate_points": aggregate_estimates["total_estimate_points"],
"backlog_issues": backlog_issues,
"total_issues": total_issues,
"completed_issues": completed_issues,
@@ -1197,15 +1157,12 @@ class CycleProgressEndpoint(BaseAPIView):
class CycleAnalyticsEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def get(self, request, slug, project_id, cycle_id):
analytic_type = request.GET.get("type", "issues")
cycle = (
Cycle.objects.filter(
workspace__slug=slug,
project_id=project_id,
id=cycle_id,
workspace__slug=slug, project_id=project_id, id=cycle_id
)
.annotate(
total_issues=Count(
@@ -1215,6 +1172,7 @@ class CycleAnalyticsEndpoint(BaseAPIView):
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -1242,6 +1200,7 @@ class CycleAnalyticsEndpoint(BaseAPIView):
assignee_distribution = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
@@ -1269,9 +1228,7 @@ class CycleAnalyticsEndpoint(BaseAPIView):
)
.values("display_name", "assignee_id", "avatar_url")
.annotate(
total_estimates=Sum(
Cast("estimate_point__value", FloatField())
)
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
)
.annotate(
completed_estimates=Sum(
@@ -1299,6 +1256,7 @@ class CycleAnalyticsEndpoint(BaseAPIView):
label_distribution = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
@@ -1307,9 +1265,7 @@ class CycleAnalyticsEndpoint(BaseAPIView):
.annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id")
.annotate(
total_estimates=Sum(
Cast("estimate_point__value", FloatField())
)
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
)
.annotate(
completed_estimates=Sum(
@@ -1345,6 +1301,7 @@ class CycleAnalyticsEndpoint(BaseAPIView):
assignee_distribution = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
project_id=project_id,
workspace__slug=slug,
)
@@ -1375,7 +1332,7 @@ class CycleAnalyticsEndpoint(BaseAPIView):
total_issues=Count(
"assignee_id",
filter=Q(archived_at__isnull=True, is_draft=False),
),
)
)
.annotate(
completed_issues=Count(
@@ -1403,6 +1360,7 @@ class CycleAnalyticsEndpoint(BaseAPIView):
label_distribution = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
project_id=project_id,
workspace__slug=slug,
)
@@ -1412,8 +1370,7 @@ class CycleAnalyticsEndpoint(BaseAPIView):
.values("label_name", "color", "label_id")
.annotate(
total_issues=Count(
"label_id",
filter=Q(archived_at__isnull=True, is_draft=False),
"label_id", filter=Q(archived_at__isnull=True, is_draft=False)
)
)
.annotate(
+33 -72
View File
@@ -3,7 +3,7 @@ import json
# Django imports
from django.core import serializers
from django.db.models import F, Func, OuterRef, Q, Case, When
from django.db.models import F, Func, OuterRef, Q, Subquery
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
@@ -12,19 +12,12 @@ from django.views.decorators.gzip import gzip_page
from rest_framework import status
from rest_framework.response import Response
# Module imports
from .. import BaseViewSet
from plane.app.serializers import (
CycleIssueSerializer,
)
from plane.app.serializers import CycleIssueSerializer
from plane.bgtasks.issue_activities_task import issue_activity
from plane.db.models import (
Cycle,
CycleIssue,
Issue,
FileAsset,
IssueLink,
)
from plane.db.models import Cycle, CycleIssue, Issue, FileAsset, IssueLink
from plane.utils.grouper import (
issue_group_values,
issue_on_results,
@@ -32,10 +25,7 @@ from plane.utils.grouper import (
)
from plane.utils.issue_filters import issue_filters
from plane.utils.order_queryset import order_issue_queryset
from plane.utils.paginator import (
GroupedOffsetPaginator,
SubGroupedOffsetPaginator,
)
from plane.utils.paginator import GroupedOffsetPaginator, SubGroupedOffsetPaginator
from plane.app.permissions import allow_permission, ROLE
@@ -46,19 +36,14 @@ class CycleIssueViewSet(BaseViewSet):
webhook_event = "cycle_issue"
bulk = True
filterset_fields = [
"issue__labels__id",
"issue__assignees__id",
]
filterset_fields = ["issue__labels__id", "issue__assignees__id"]
def get_queryset(self):
return self.filter_queryset(
super()
.get_queryset()
.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("issue_id")
)
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue_id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -80,35 +65,27 @@ class CycleIssueViewSet(BaseViewSet):
)
@method_decorator(gzip_page)
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
]
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def list(self, request, slug, project_id, cycle_id):
order_by_param = request.GET.get("order_by", "created_at")
filters = issue_filters(request.query_params, "GET")
issue_queryset = (
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id, issue_cycle__deleted_at__isnull=True
)
.filter(project_id=project_id)
.filter(workspace__slug=slug)
.filter(**filters)
.select_related("workspace", "project", "state", "parent")
.prefetch_related(
"assignees",
"labels",
"issue_module__module",
"issue_cycle__cycle",
"assignees", "labels", "issue_module__module", "issue_cycle__cycle"
)
.filter(**filters)
.annotate(
cycle_id=Case(
When(
issue_cycle__cycle__deleted_at__isnull=True,
then=F("issue_cycle__cycle_id"),
),
default=None,
cycle_id=Subquery(
CycleIssue.objects.filter(
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
)
)
.annotate(
@@ -127,9 +104,7 @@ class CycleIssueViewSet(BaseViewSet):
.values("count")
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -141,8 +116,7 @@ class CycleIssueViewSet(BaseViewSet):
issue_queryset = issue_queryset.filter(**filters)
# Issue queryset
issue_queryset, order_by_param = order_issue_queryset(
issue_queryset=issue_queryset,
order_by_param=order_by_param,
issue_queryset=issue_queryset, order_by_param=order_by_param
)
# Group by
@@ -151,9 +125,7 @@ class CycleIssueViewSet(BaseViewSet):
# issue queryset
issue_queryset = issue_queryset_grouper(
queryset=issue_queryset,
group_by=group_by,
sub_group_by=sub_group_by,
queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by
)
if group_by:
@@ -173,9 +145,7 @@ class CycleIssueViewSet(BaseViewSet):
order_by=order_by_param,
queryset=issue_queryset,
on_results=lambda issues: issue_on_results(
group_by=group_by,
issues=issues,
sub_group_by=sub_group_by,
group_by=group_by, issues=issues, sub_group_by=sub_group_by
),
paginator_cls=SubGroupedOffsetPaginator,
group_by_fields=issue_group_values(
@@ -193,10 +163,10 @@ class CycleIssueViewSet(BaseViewSet):
group_by_field_name=group_by,
sub_group_by_field_name=sub_group_by,
count_filter=Q(
Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1)
| Q(issue_inbox__status=2)
| Q(issue_inbox__isnull=True),
Q(issue_intake__status=1)
| Q(issue_intake__status=-1)
| Q(issue_intake__status=2)
| Q(issue_intake__isnull=True),
archived_at__isnull=True,
is_draft=False,
),
@@ -209,9 +179,7 @@ class CycleIssueViewSet(BaseViewSet):
order_by=order_by_param,
queryset=issue_queryset,
on_results=lambda issues: issue_on_results(
group_by=group_by,
issues=issues,
sub_group_by=sub_group_by,
group_by=group_by, issues=issues, sub_group_by=sub_group_by
),
paginator_cls=GroupedOffsetPaginator,
group_by_fields=issue_group_values(
@@ -222,10 +190,10 @@ class CycleIssueViewSet(BaseViewSet):
),
group_by_field_name=group_by,
count_filter=Q(
Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1)
| Q(issue_inbox__status=2)
| Q(issue_inbox__isnull=True),
Q(issue_intake__status=1)
| Q(issue_intake__status=-1)
| Q(issue_intake__status=2)
| Q(issue_intake__isnull=True),
archived_at__isnull=True,
is_draft=False,
),
@@ -247,8 +215,7 @@ class CycleIssueViewSet(BaseViewSet):
if not issues:
return Response(
{"error": "Issues are required"},
status=status.HTTP_400_BAD_REQUEST,
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
)
cycle = Cycle.objects.get(
@@ -265,13 +232,9 @@ class CycleIssueViewSet(BaseViewSet):
# Get all CycleIssues already created
cycle_issues = list(
CycleIssue.objects.filter(
~Q(cycle_id=cycle_id), issue_id__in=issues
)
CycleIssue.objects.filter(~Q(cycle_id=cycle_id), issue_id__in=issues)
)
existing_issues = [
str(cycle_issue.issue_id) for cycle_issue in cycle_issues
]
existing_issues = [str(cycle_issue.issue_id) for cycle_issue in cycle_issues]
new_issues = list(set(issues) - set(existing_issues))
# New issues to create
@@ -310,9 +273,7 @@ class CycleIssueViewSet(BaseViewSet):
)
# Update the cycle issues
CycleIssue.objects.bulk_update(
updated_records, ["cycle_id"], batch_size=100
)
CycleIssue.objects.bulk_update(updated_records, ["cycle_id"], batch_size=100)
# Capture Issue Activity
issue_activity.delay(
type="cycle.activity.created",
+60 -79
View File
@@ -42,6 +42,7 @@ from plane.db.models import (
Project,
Widget,
WorkspaceMember,
CycleIssue,
)
from plane.utils.issue_filters import issue_filters
@@ -191,7 +192,13 @@ def dashboard_assigned_issues(self, request, slug):
).select_related("issue"),
)
)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(
cycle_id=Subquery(
CycleIssue.objects.filter(
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
)
)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
@@ -218,9 +225,9 @@ def dashboard_assigned_issues(self, request, slug):
ArrayAgg(
"labels__id",
distinct=True,
filter=(
filter=Q(
~Q(labels__id__isnull=True)
& Q(labels__deleted_at__isnull=True)
& Q(label_issue__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
@@ -229,8 +236,11 @@ def dashboard_assigned_issues(self, request, slug):
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True),
filter=Q(
~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True)
& Q(issue_assignee__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -238,9 +248,11 @@ def dashboard_assigned_issues(self, request, slug):
ArrayAgg(
"issue_module__module_id",
distinct=True,
filter=~Q(issue_module__module_id__isnull=True)
& Q(issue_module__module__archived_at__isnull=True)
& Q(issue_module__module__deleted_at__isnull=True),
filter=Q(
~Q(issue_module__module_id__isnull=True)
& Q(issue_module__module__archived_at__isnull=True)
& Q(issue_module__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -248,10 +260,7 @@ def dashboard_assigned_issues(self, request, slug):
)
if WorkspaceMember.objects.filter(
workspace__slug=slug,
member=request.user,
role=5,
is_active=True,
workspace__slug=slug, member=request.user, role=5, is_active=True
).exists():
assigned_issues = assigned_issues.filter(created_by=request.user)
@@ -259,10 +268,7 @@ def dashboard_assigned_issues(self, request, slug):
priority_order = ["urgent", "high", "medium", "low", "none"]
assigned_issues = assigned_issues.annotate(
priority_order=Case(
*[
When(priority=p, then=Value(i))
for i, p in enumerate(priority_order)
],
*[When(priority=p, then=Value(i)) for i, p in enumerate(priority_order)],
output_field=CharField(),
)
).order_by("priority_order")
@@ -288,9 +294,7 @@ def dashboard_assigned_issues(self, request, slug):
completed_issues_count = assigned_issues.filter(
state__group__in=["completed"]
).count()
completed_issues = assigned_issues.filter(
state__group__in=["completed"]
)[:5]
completed_issues = assigned_issues.filter(state__group__in=["completed"])[:5]
return Response(
{
"issues": IssueSerializer(
@@ -360,7 +364,13 @@ def dashboard_created_issues(self, request, slug):
.filter(**filters)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(
cycle_id=Subquery(
CycleIssue.objects.filter(
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
)
)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
@@ -387,9 +397,9 @@ def dashboard_created_issues(self, request, slug):
ArrayAgg(
"labels__id",
distinct=True,
filter=(
filter=Q(
~Q(labels__id__isnull=True)
& Q(labels__deleted_at__isnull=True)
& Q(label_issue__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
@@ -398,8 +408,11 @@ def dashboard_created_issues(self, request, slug):
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True),
filter=Q(
~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True)
& Q(issue_assignee__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -407,9 +420,11 @@ def dashboard_created_issues(self, request, slug):
ArrayAgg(
"issue_module__module_id",
distinct=True,
filter=~Q(issue_module__module_id__isnull=True)
& Q(issue_module__module__archived_at__isnull=True)
& Q(issue_module__module__deleted_at__isnull=True),
filter=Q(
~Q(issue_module__module_id__isnull=True)
& Q(issue_module__module__archived_at__isnull=True)
& Q(issue_module__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -421,10 +436,7 @@ def dashboard_created_issues(self, request, slug):
priority_order = ["urgent", "high", "medium", "low", "none"]
created_issues = created_issues.annotate(
priority_order=Case(
*[
When(priority=p, then=Value(i))
for i, p in enumerate(priority_order)
],
*[When(priority=p, then=Value(i)) for i, p in enumerate(priority_order)],
output_field=CharField(),
)
).order_by("priority_order")
@@ -450,9 +462,7 @@ def dashboard_created_issues(self, request, slug):
completed_issues_count = created_issues.filter(
state__group__in=["completed"]
).count()
completed_issues = created_issues.filter(
state__group__in=["completed"]
)[:5]
completed_issues = created_issues.filter(state__group__in=["completed"])[:5]
return Response(
{
"issues": IssueSerializer(completed_issues, many=True).data,
@@ -507,10 +517,7 @@ def dashboard_issues_by_state_groups(self, request, slug):
extra_filters = {}
if WorkspaceMember.objects.filter(
workspace__slug=slug,
member=request.user,
role=5,
is_active=True,
workspace__slug=slug, member=request.user, role=5, is_active=True
).exists():
extra_filters = {"created_by": request.user}
@@ -547,10 +554,7 @@ def dashboard_issues_by_priority(self, request, slug):
extra_filters = {}
if WorkspaceMember.objects.filter(
workspace__slug=slug,
member=request.user,
role=5,
is_active=True,
workspace__slug=slug, member=request.user, role=5, is_active=True
).exists():
extra_filters = {"created_by": request.user}
@@ -575,8 +579,7 @@ def dashboard_issues_by_priority(self, request, slug):
# Prepare output including all groups with their counts
output_data = [
{"priority": group, "count": count}
for group, count in all_groups.items()
{"priority": group, "count": count} for group, count in all_groups.items()
]
return Response(output_data, status=status.HTTP_200_OK)
@@ -593,8 +596,7 @@ def dashboard_recent_activity(self, request, slug):
).select_related("actor", "workspace", "issue", "project")[:8]
return Response(
IssueActivitySerializer(queryset, many=True).data,
status=status.HTTP_200_OK,
IssueActivitySerializer(queryset, many=True).data, status=status.HTTP_200_OK
)
@@ -624,22 +626,14 @@ def dashboard_recent_projects(self, request, slug):
).exclude(id__in=unique_project_ids)
# Append additional project IDs to the existing list
unique_project_ids.update(
additional_projects.values_list("id", flat=True)
)
unique_project_ids.update(additional_projects.values_list("id", flat=True))
return Response(
list(unique_project_ids)[:4],
status=status.HTTP_200_OK,
)
return Response(list(unique_project_ids)[:4], status=status.HTTP_200_OK)
def dashboard_recent_collaborators(self, request, slug):
project_members_with_activities = (
WorkspaceMember.objects.filter(
workspace__slug=slug,
is_active=True,
)
WorkspaceMember.objects.filter(workspace__slug=slug, is_active=True)
.annotate(
active_issue_count=Count(
Case(
@@ -664,10 +658,7 @@ def dashboard_recent_collaborators(self, request, slug):
.order_by("-active_issue_count")
.distinct()
)
return Response(
(project_members_with_activities),
status=status.HTTP_200_OK,
)
return Response((project_members_with_activities), status=status.HTTP_200_OK)
class DashboardEndpoint(BaseAPIView):
@@ -716,14 +707,13 @@ class DashboardEndpoint(BaseAPIView):
updated_dashboard_widgets = []
for widget_key in widgets_to_fetch:
widget = Widget.objects.filter(
key=widget_key
).values_list("id", flat=True)
widget = Widget.objects.filter(key=widget_key).values_list(
"id", flat=True
)
if widget:
updated_dashboard_widgets.append(
DashboardWidget(
widget_id=widget,
dashboard_id=dashboard.id,
widget_id=widget, dashboard_id=dashboard.id
)
)
@@ -790,11 +780,7 @@ class DashboardEndpoint(BaseAPIView):
func = WIDGETS_MAPPER.get(widget_key)
if func is not None:
response = func(
self,
request=request,
slug=slug,
)
response = func(self, request=request, slug=slug)
if isinstance(response, Response):
return response
@@ -807,8 +793,7 @@ class DashboardEndpoint(BaseAPIView):
class WidgetsEndpoint(BaseAPIView):
def patch(self, request, dashboard_id, widget_id):
dashboard_widget = DashboardWidget.objects.filter(
widget_id=widget_id,
dashboard_id=dashboard_id,
widget_id=widget_id, dashboard_id=dashboard_id
).first()
dashboard_widget.is_visible = request.data.get(
"is_visible", dashboard_widget.is_visible
@@ -816,10 +801,6 @@ class WidgetsEndpoint(BaseAPIView):
dashboard_widget.sort_order = request.data.get(
"sort_order", dashboard_widget.sort_order
)
dashboard_widget.filters = request.data.get(
"filters", dashboard_widget.filters
)
dashboard_widget.filters = request.data.get("filters", dashboard_widget.filters)
dashboard_widget.save()
return Response(
{"message": "successfully updated"}, status=status.HTTP_200_OK
)
return Response({"message": "successfully updated"}, status=status.HTTP_200_OK)
+1
View File
@@ -1,5 +1,6 @@
# views.py
from django.http import JsonResponse
def custom_404_view(request, exception=None):
return JsonResponse({"error": "Page not found."}, status=404)
+23 -75
View File
@@ -11,11 +11,7 @@ from rest_framework import status
# Module imports
from ..base import BaseViewSet, BaseAPIView
from plane.app.permissions import (
ProjectEntityPermission,
allow_permission,
ROLE,
)
from plane.app.permissions import ProjectEntityPermission, allow_permission, ROLE
from plane.db.models import Project, Estimate, EstimatePoint, Issue
from plane.app.serializers import (
EstimateSerializer,
@@ -32,13 +28,7 @@ def generate_random_name(length=10):
class ProjectEstimatePointEndpoint(BaseAPIView):
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
]
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def get(self, request, slug, project_id):
project = Project.objects.get(workspace__slug=slug, pk=project_id)
if project.estimate_id is not None:
@@ -53,17 +43,13 @@ class ProjectEstimatePointEndpoint(BaseAPIView):
class BulkEstimatePointEndpoint(BaseViewSet):
permission_classes = [
ProjectEntityPermission,
]
permission_classes = [ProjectEntityPermission]
model = Estimate
serializer_class = EstimateSerializer
def list(self, request, slug, project_id):
estimates = (
Estimate.objects.filter(
workspace__slug=slug, project_id=project_id
)
Estimate.objects.filter(workspace__slug=slug, project_id=project_id)
.prefetch_related("points")
.select_related("workspace", "project")
)
@@ -91,9 +77,7 @@ class BulkEstimatePointEndpoint(BaseViewSet):
data=request.data.get("estimate_points"), many=True
)
if not serializer.is_valid():
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
estimate_points = EstimatePoint.objects.bulk_create(
[
@@ -121,16 +105,12 @@ class BulkEstimatePointEndpoint(BaseViewSet):
pk=estimate_id, workspace__slug=slug, project_id=project_id
)
serializer = EstimateReadSerializer(estimate)
return Response(
serializer.data,
status=status.HTTP_200_OK,
)
return Response(serializer.data, status=status.HTTP_200_OK)
@invalidate_cache(
path="/api/workspaces/:slug/estimates/", url_params=True, user=False
)
def partial_update(self, request, slug, project_id, estimate_id):
if not len(request.data.get("estimate_points", [])):
return Response(
{"error": "Estimate points are required"},
@@ -140,20 +120,15 @@ class BulkEstimatePointEndpoint(BaseViewSet):
estimate = Estimate.objects.get(pk=estimate_id)
if request.data.get("estimate"):
estimate.name = request.data.get("estimate").get(
"name", estimate.name
)
estimate.type = request.data.get("estimate").get(
"type", estimate.type
)
estimate.name = request.data.get("estimate").get("name", estimate.name)
estimate.type = request.data.get("estimate").get("type", estimate.type)
estimate.save()
estimate_points_data = request.data.get("estimate_points", [])
estimate_points = EstimatePoint.objects.filter(
pk__in=[
estimate_point.get("id")
for estimate_point in estimate_points_data
estimate_point.get("id") for estimate_point in estimate_points_data
],
workspace__slug=slug,
project_id=project_id,
@@ -178,16 +153,11 @@ class BulkEstimatePointEndpoint(BaseViewSet):
updated_estimate_points.append(estimate_point)
EstimatePoint.objects.bulk_update(
updated_estimate_points,
["key", "value"],
batch_size=10,
updated_estimate_points, ["key", "value"], batch_size=10
)
estimate_serializer = EstimateReadSerializer(estimate)
return Response(
estimate_serializer.data,
status=status.HTTP_200_OK,
)
return Response(estimate_serializer.data, status=status.HTTP_200_OK)
@invalidate_cache(
path="/api/workspaces/:slug/estimates/", url_params=True, user=False
@@ -201,7 +171,6 @@ class BulkEstimatePointEndpoint(BaseViewSet):
class EstimatePointEndpoint(BaseViewSet):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def create(self, request, slug, project_id, estimate_id):
# TODO: add a key validation if the same key already exists
@@ -213,18 +182,13 @@ class EstimatePointEndpoint(BaseViewSet):
key = request.data.get("key", 0)
value = request.data.get("value", "")
estimate_point = EstimatePoint.objects.create(
estimate_id=estimate_id,
project_id=project_id,
key=key,
value=value,
estimate_id=estimate_id, project_id=project_id, key=key, value=value
)
serializer = EstimatePointSerializer(estimate_point).data
return Response(serializer, status=status.HTTP_200_OK)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def partial_update(
self, request, slug, project_id, estimate_id, estimate_point_id
):
def partial_update(self, request, slug, project_id, estimate_id, estimate_point_id):
# TODO: add a key validation if the same key already exists
estimate_point = EstimatePoint.objects.get(
pk=estimate_point_id,
@@ -236,21 +200,15 @@ class EstimatePointEndpoint(BaseViewSet):
estimate_point, data=request.data, partial=True
)
if not serializer.is_valid():
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def destroy(
self, request, slug, project_id, estimate_id, estimate_point_id
):
def destroy(self, request, slug, project_id, estimate_id, estimate_point_id):
new_estimate_id = request.data.get("new_estimate_id", None)
estimate_points = EstimatePoint.objects.filter(
estimate_id=estimate_id,
project_id=project_id,
workspace__slug=slug,
estimate_id=estimate_id, project_id=project_id, workspace__slug=slug
)
# update all the issues with the new estimate
if new_estimate_id:
@@ -265,10 +223,8 @@ class EstimatePointEndpoint(BaseViewSet):
requested_data=json.dumps(
{
"estimate_point": (
str(new_estimate_id)
if new_estimate_id
else None
),
str(new_estimate_id) if new_estimate_id else None
)
}
),
actor_id=str(request.user.id),
@@ -280,7 +236,7 @@ class EstimatePointEndpoint(BaseViewSet):
str(issue.estimate_point_id)
if issue.estimate_point_id
else None
),
)
}
),
epoch=int(timezone.now().timestamp()),
@@ -295,11 +251,7 @@ class EstimatePointEndpoint(BaseViewSet):
for issue in issues:
issue_activity.delay(
type="issue.activity.updated",
requested_data=json.dumps(
{
"estimate_point": None,
}
),
requested_data=json.dumps({"estimate_point": None}),
actor_id=str(request.user.id),
issue_id=issue.id,
project_id=str(project_id),
@@ -309,16 +261,14 @@ class EstimatePointEndpoint(BaseViewSet):
str(issue.estimate_point_id)
if issue.estimate_point_id
else None
),
)
}
),
epoch=int(timezone.now().timestamp()),
)
# delete the estimate point
old_estimate_point = EstimatePoint.objects.filter(
pk=estimate_point_id
).first()
old_estimate_point = EstimatePoint.objects.filter(pk=estimate_point_id).first()
# rearrange the estimate points
updated_estimate_points = []
@@ -328,9 +278,7 @@ class EstimatePointEndpoint(BaseViewSet):
updated_estimate_points.append(estimate_point)
EstimatePoint.objects.bulk_update(
updated_estimate_points,
["key"],
batch_size=10,
updated_estimate_points, ["key"], batch_size=10
)
old_estimate_point.delete()
+4 -11
View File
@@ -51,9 +51,7 @@ class ExportIssuesEndpoint(BaseAPIView):
slug=slug,
)
return Response(
{
"message": "Once the export is ready you will be able to download it"
},
{"message": "Once the export is ready you will be able to download it"},
status=status.HTTP_200_OK,
)
else:
@@ -62,18 +60,13 @@ class ExportIssuesEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST,
)
@allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE"
)
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def get(self, request, slug):
exporter_history = ExporterHistory.objects.filter(
workspace__slug=slug,
type="issue_exports",
workspace__slug=slug, type="issue_exports"
).select_related("workspace", "initiated_by")
if request.GET.get("per_page", False) and request.GET.get(
"cursor", False
):
if request.GET.get("per_page", False) and request.GET.get("cursor", False):
return self.paginate(
order_by=request.GET.get("order_by", "-created_at"),
request=request,
+10 -31
View File
@@ -13,15 +13,11 @@ from rest_framework import status
from ..base import BaseAPIView
from plane.app.permissions import allow_permission, ROLE
from plane.db.models import Workspace, Project
from plane.app.serializers import (
ProjectLiteSerializer,
WorkspaceLiteSerializer,
)
from plane.app.serializers import ProjectLiteSerializer, WorkspaceLiteSerializer
from plane.license.utils.instance_value import get_configuration_value
class GPTIntegrationEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def post(self, request, slug, project_id):
OPENAI_API_KEY, GPT_ENGINE = get_configuration_value(
@@ -50,19 +46,15 @@ class GPTIntegrationEndpoint(BaseAPIView):
if not task:
return Response(
{"error": "Task is required"},
status=status.HTTP_400_BAD_REQUEST,
{"error": "Task is required"}, status=status.HTTP_400_BAD_REQUEST
)
final_text = task + "\n" + prompt
client = OpenAI(
api_key=OPENAI_API_KEY,
)
client = OpenAI(api_key=OPENAI_API_KEY)
response = client.chat.completions.create(
model=GPT_ENGINE,
messages=[{"role": "user", "content": final_text}],
model=GPT_ENGINE, messages=[{"role": "user", "content": final_text}]
)
workspace = Workspace.objects.get(slug=slug)
@@ -82,10 +74,7 @@ class GPTIntegrationEndpoint(BaseAPIView):
class WorkspaceGPTIntegrationEndpoint(BaseAPIView):
@allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE"
)
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def post(self, request, slug):
OPENAI_API_KEY, GPT_ENGINE = get_configuration_value(
[
@@ -113,29 +102,21 @@ class WorkspaceGPTIntegrationEndpoint(BaseAPIView):
if not task:
return Response(
{"error": "Task is required"},
status=status.HTTP_400_BAD_REQUEST,
{"error": "Task is required"}, status=status.HTTP_400_BAD_REQUEST
)
final_text = task + "\n" + prompt
client = OpenAI(
api_key=OPENAI_API_KEY,
)
client = OpenAI(api_key=OPENAI_API_KEY)
response = client.chat.completions.create(
model=GPT_ENGINE,
messages=[{"role": "user", "content": final_text}],
model=GPT_ENGINE, messages=[{"role": "user", "content": final_text}]
)
text = response.choices[0].message.content.strip()
text_html = text.replace("\n", "<br/>")
return Response(
{
"response": text,
"response_html": text_html,
},
status=status.HTTP_200_OK,
{"response": text, "response_html": text_html}, status=status.HTTP_200_OK
)
@@ -164,9 +145,7 @@ class UnsplashEndpoint(BaseAPIView):
else f"https://api.unsplash.com/photos/?client_id={UNSPLASH_ACCESS_KEY}&page={page}&per_page={per_page}"
)
headers = {
"Content-Type": "application/json",
}
headers = {"Content-Type": "application/json"}
resp = requests.get(url=url, headers=headers)
return Response(resp.json(), status=resp.status_code)
@@ -3,7 +3,7 @@ import json
# Django import
from django.utils import timezone
from django.db.models import Q, Count, OuterRef, Func, F, Prefetch, Case, When
from django.db.models import Q, Count, OuterRef, Func, F, Prefetch, Subquery
from django.core.serializers.json import DjangoJSONEncoder
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
@@ -18,30 +18,30 @@ from rest_framework.response import Response
from ..base import BaseViewSet
from plane.app.permissions import allow_permission, ROLE
from plane.db.models import (
Inbox,
InboxIssue,
Intake,
IntakeIssue,
Issue,
State,
IssueLink,
FileAsset,
Project,
ProjectMember,
CycleIssue,
)
from plane.app.serializers import (
IssueCreateSerializer,
IssueSerializer,
InboxSerializer,
InboxIssueSerializer,
InboxIssueDetailSerializer,
IntakeSerializer,
IntakeIssueSerializer,
IntakeIssueDetailSerializer,
)
from plane.utils.issue_filters import issue_filters
from plane.bgtasks.issue_activities_task import issue_activity
class InboxViewSet(BaseViewSet):
serializer_class = InboxSerializer
model = Inbox
class IntakeViewSet(BaseViewSet):
serializer_class = IntakeSerializer
model = Intake
def get_queryset(self):
return (
@@ -53,8 +53,7 @@ class InboxViewSet(BaseViewSet):
)
.annotate(
pending_issue_count=Count(
"issue_inbox",
filter=Q(issue_inbox__status=-2),
"issue_intake", filter=Q(issue_intake__status=-2)
)
)
.select_related("workspace", "project")
@@ -62,11 +61,8 @@ class InboxViewSet(BaseViewSet):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def list(self, request, slug, project_id):
inbox = self.get_queryset().first()
return Response(
InboxSerializer(inbox).data,
status=status.HTTP_200_OK,
)
intake = self.get_queryset().first()
return Response(IntakeSerializer(intake).data, status=status.HTTP_200_OK)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def perform_create(self, serializer):
@@ -74,27 +70,24 @@ class InboxViewSet(BaseViewSet):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def destroy(self, request, slug, project_id, pk):
inbox = Inbox.objects.filter(
intake = Intake.objects.filter(
workspace__slug=slug, project_id=project_id, pk=pk
).first()
# Handle default inbox delete
if inbox.is_default:
# Handle default intake delete
if intake.is_default:
return Response(
{"error": "You cannot delete the default inbox"},
{"error": "You cannot delete the default intake"},
status=status.HTTP_400_BAD_REQUEST,
)
inbox.delete()
intake.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class InboxIssueViewSet(BaseViewSet):
class IntakeIssueViewSet(BaseViewSet):
serializer_class = IntakeIssueSerializer
model = IntakeIssue
serializer_class = InboxIssueSerializer
model = InboxIssue
filterset_fields = [
"status",
]
filterset_fields = ["statulls"]
def get_queryset(self):
return (
@@ -106,19 +99,17 @@ class InboxIssueViewSet(BaseViewSet):
.prefetch_related("assignees", "labels", "issue_module__module")
.prefetch_related(
Prefetch(
"issue_inbox",
queryset=InboxIssue.objects.only(
"issue_intake",
queryset=IntakeIssue.objects.only(
"status", "duplicate_to", "snoozed_till", "source"
),
)
)
.annotate(
cycle_id=Case(
When(
issue_cycle__cycle__deleted_at__isnull=True,
then=F("issue_cycle__cycle_id"),
),
default=None,
cycle_id=Subquery(
CycleIssue.objects.filter(
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
)
)
.annotate(
@@ -137,9 +128,7 @@ class InboxIssueViewSet(BaseViewSet):
.values("count")
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -149,9 +138,9 @@ class InboxIssueViewSet(BaseViewSet):
ArrayAgg(
"labels__id",
distinct=True,
filter=(
filter=Q(
~Q(labels__id__isnull=True)
& Q(labels__deleted_at__isnull=True)
& Q(label_issue__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
@@ -160,8 +149,11 @@ class InboxIssueViewSet(BaseViewSet):
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True),
filter=Q(
~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True)
& Q(issue_assignee__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -169,9 +161,11 @@ class InboxIssueViewSet(BaseViewSet):
ArrayAgg(
"issue_module__module_id",
distinct=True,
filter=~Q(issue_module__module_id__isnull=True)
& Q(issue_module__module__archived_at__isnull=True)
& Q(issue_module__module__deleted_at__isnull=True),
filter=Q(
~Q(issue_module__module_id__isnull=True)
& Q(issue_module__module__archived_at__isnull=True)
& Q(issue_module__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -180,39 +174,39 @@ class InboxIssueViewSet(BaseViewSet):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def list(self, request, slug, project_id):
inbox_id = Inbox.objects.filter(
intake_id = Intake.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
project = Project.objects.get(pk=project_id)
filters = issue_filters(request.GET, "GET", "issue__")
inbox_issue = (
InboxIssue.objects.filter(
inbox_id=inbox_id.id, project_id=project_id, **filters
intake_issue = (
IntakeIssue.objects.filter(
intake_id=intake_id.id, project_id=project_id, **filters
)
.select_related("issue")
.prefetch_related(
"issue__labels",
)
.prefetch_related("issue__labels")
.annotate(
label_ids=Coalesce(
ArrayAgg(
"issue__labels__id",
distinct=True,
filter=~Q(issue__labels__id__isnull=True)
& Q(issue__labels__deleted_at__isnull=True),
filter=Q(
~Q(issue__labels__id__isnull=True)
& Q(issue__label_issue__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
)
)
).order_by(request.GET.get("order_by", "-issue__created_at"))
# inbox status filter
inbox_status = [
# Intake status filter
intake_status = [
item
for item in request.GET.get("status", "-2").split(",")
if item != "null"
]
if inbox_status:
inbox_issue = inbox_issue.filter(status__in=inbox_status)
if intake_status:
intake_issue = intake_issue.filter(status__in=intake_status)
if (
ProjectMember.objects.filter(
@@ -224,13 +218,12 @@ class InboxIssueViewSet(BaseViewSet):
).exists()
and not project.guest_view_all_features
):
inbox_issue = inbox_issue.filter(created_by=request.user)
intake_issue = intake_issue.filter(created_by=request.user)
return self.paginate(
request=request,
queryset=(inbox_issue),
on_results=lambda inbox_issues: InboxIssueSerializer(
inbox_issues,
many=True,
queryset=(intake_issue),
on_results=lambda intake_issues: IntakeIssueSerializer(
intake_issues, many=True
).data,
)
@@ -238,8 +231,7 @@ class InboxIssueViewSet(BaseViewSet):
def create(self, request, slug, project_id):
if not request.data.get("issue", {}).get("name", False):
return Response(
{"error": "Name is required"},
status=status.HTTP_400_BAD_REQUEST,
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
)
# Check for valid priority
@@ -251,20 +243,9 @@ class InboxIssueViewSet(BaseViewSet):
"none",
]:
return Response(
{"error": "Invalid priority"},
status=status.HTTP_400_BAD_REQUEST,
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
)
# Create or get state
state, _ = State.objects.get_or_create(
name="Triage",
group="triage",
description="Default state for managing all Inbox Issues",
project_id=project_id,
color="#ff7700",
is_triage=True,
)
# create an issue
project = Project.objects.get(pk=project_id)
serializer = IssueCreateSerializer(
@@ -277,15 +258,15 @@ class InboxIssueViewSet(BaseViewSet):
)
if serializer.is_valid():
serializer.save()
inbox_id = Inbox.objects.filter(
intake_id = Intake.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
# create an inbox issue
inbox_issue = InboxIssue.objects.create(
inbox_id=inbox_id.id,
# create an intake issue
intake_issue = IntakeIssue.objects.create(
intake_id=intake_id.id,
project_id=project_id,
issue_id=serializer.data["id"],
source=request.data.get("source", "in-app"),
source=request.data.get("source", "IN-APP"),
)
# Create an Issue Activity
issue_activity.delay(
@@ -298,22 +279,19 @@ class InboxIssueViewSet(BaseViewSet):
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
inbox=str(inbox_issue.id),
intake=str(intake_issue.id),
)
inbox_issue = (
InboxIssue.objects.select_related("issue")
.prefetch_related(
"issue__labels",
"issue__assignees",
)
intake_issue = (
IntakeIssue.objects.select_related("issue")
.prefetch_related("issue__labels", "issue__assignees")
.annotate(
label_ids=Coalesce(
ArrayAgg(
"issue__labels__id",
distinct=True,
filter=(
filter=Q(
~Q(issue__labels__id__isnull=True)
& Q(issue__labels__deleted_at__isnull=True)
& Q(issue__label_issue__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
@@ -329,28 +307,26 @@ class InboxIssueViewSet(BaseViewSet):
),
)
.get(
inbox_id=inbox_id.id,
intake_id=intake_id.id,
issue_id=serializer.data["id"],
project_id=project_id,
)
)
serializer = InboxIssueDetailSerializer(inbox_issue)
serializer = IntakeIssueDetailSerializer(intake_issue)
return Response(serializer.data, status=status.HTTP_200_OK)
else:
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=Issue)
def partial_update(self, request, slug, project_id, pk):
inbox_id = Inbox.objects.filter(
intake_id = Intake.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
inbox_issue = InboxIssue.objects.get(
intake_issue = IntakeIssue.objects.get(
issue_id=pk,
workspace__slug=slug,
project_id=project_id,
inbox_id=inbox_id,
intake_id=intake_id,
)
# Get the project member
project_member = ProjectMember.objects.get(
@@ -360,11 +336,11 @@ class InboxIssueViewSet(BaseViewSet):
is_active=True,
)
# Only project members admins and created_by users can access this endpoint
if project_member.role <= 5 and str(inbox_issue.created_by_id) != str(
if project_member.role <= 5 and str(intake_issue.created_by_id) != str(
request.user.id
):
return Response(
{"error": "You cannot edit inbox issues"},
{"error": "You cannot edit intake issues"},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -376,7 +352,10 @@ class InboxIssueViewSet(BaseViewSet):
ArrayAgg(
"labels__id",
distinct=True,
filter=~Q(labels__id__isnull=True),
filter=Q(
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -384,15 +363,14 @@ class InboxIssueViewSet(BaseViewSet):
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True),
filter=Q(
~Q(assignees__id__isnull=True)
& Q(issue_assignee__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
).get(
pk=inbox_issue.issue_id,
workspace__slug=slug,
project_id=project_id,
)
).get(pk=intake_issue.issue_id, workspace__slug=slug, project_id=project_id)
# Only allow guests to edit name and description
if project_member.role <= 5:
issue_data = {
@@ -400,9 +378,7 @@ class InboxIssueViewSet(BaseViewSet):
"description_html": issue_data.get(
"description_html", issue.description_html
),
"description": issue_data.get(
"description", issue.description
),
"description": issue_data.get("description", issue.description),
}
issue_serializer = IssueCreateSerializer(
@@ -427,7 +403,7 @@ class InboxIssueViewSet(BaseViewSet):
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
inbox=str(inbox_issue.id),
intake=str(intake_issue.id),
)
issue_serializer.save()
else:
@@ -435,27 +411,25 @@ class InboxIssueViewSet(BaseViewSet):
issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
# Only project admins and members can edit inbox issue attributes
# Only project admins and members can edit intake issue attributes
if project_member.role > 15:
serializer = InboxIssueSerializer(
inbox_issue, data=request.data, partial=True
serializer = IntakeIssueSerializer(
intake_issue, data=request.data, partial=True
)
current_instance = json.dumps(
InboxIssueSerializer(inbox_issue).data, cls=DjangoJSONEncoder
IntakeIssueSerializer(intake_issue).data, cls=DjangoJSONEncoder
)
if serializer.is_valid():
serializer.save()
# Update the issue state if the issue is rejected or marked as duplicate
if serializer.data["status"] in [-1, 2]:
issue = Issue.objects.get(
pk=inbox_issue.issue_id,
pk=intake_issue.issue_id,
workspace__slug=slug,
project_id=project_id,
)
state = State.objects.filter(
group="cancelled",
workspace__slug=slug,
project_id=project_id,
group="cancelled", workspace__slug=slug, project_id=project_id
).first()
if state is not None:
issue.state = state
@@ -464,7 +438,7 @@ class InboxIssueViewSet(BaseViewSet):
# Update the issue state if it is accepted
if serializer.data["status"] in [1]:
issue = Issue.objects.get(
pk=inbox_issue.issue_id,
pk=intake_issue.issue_id,
workspace__slug=slug,
project_id=project_id,
)
@@ -473,19 +447,15 @@ class InboxIssueViewSet(BaseViewSet):
if issue.state.is_triage:
# Move to default state
state = State.objects.filter(
workspace__slug=slug,
project_id=project_id,
default=True,
workspace__slug=slug, project_id=project_id, default=True
).first()
if state is not None:
issue.state = state
issue.save()
# create a activity for status change
issue_activity.delay(
type="inbox.activity.created",
requested_data=json.dumps(
request.data, cls=DjangoJSONEncoder
),
type="intake.activity.created",
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
actor_id=str(request.user.id),
issue_id=str(pk),
project_id=str(project_id),
@@ -493,21 +463,21 @@ class InboxIssueViewSet(BaseViewSet):
epoch=int(timezone.now().timestamp()),
notification=False,
origin=request.META.get("HTTP_ORIGIN"),
inbox=(inbox_issue.id),
intake=(intake_issue.id),
)
inbox_issue = (
InboxIssue.objects.select_related("issue")
.prefetch_related(
"issue__labels",
"issue__assignees",
)
intake_issue = (
IntakeIssue.objects.select_related("issue")
.prefetch_related("issue__labels", "issue__assignees")
.annotate(
label_ids=Coalesce(
ArrayAgg(
"issue__labels__id",
distinct=True,
filter=~Q(issue__labels__id__isnull=True),
filter=Q(
~Q(issue__labels__id__isnull=True)
& Q(issue__label_issue__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -515,52 +485,43 @@ class InboxIssueViewSet(BaseViewSet):
ArrayAgg(
"issue__assignees__id",
distinct=True,
filter=~Q(issue__assignees__id__isnull=True),
filter=Q(
~Q(issue__assignees__id__isnull=True)
& Q(issue__issue_assignee__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
)
.get(
inbox_id=inbox_id.id,
issue_id=pk,
project_id=project_id,
)
.get(intake_id=intake_id.id, issue_id=pk, project_id=project_id)
)
serializer = InboxIssueDetailSerializer(inbox_issue).data
serializer = IntakeIssueDetailSerializer(intake_issue).data
return Response(serializer, status=status.HTTP_200_OK)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else:
serializer = InboxIssueDetailSerializer(inbox_issue).data
serializer = IntakeIssueDetailSerializer(intake_issue).data
return Response(serializer, status=status.HTTP_200_OK)
@allow_permission(
allowed_roles=[
ROLE.ADMIN,
ROLE.MEMBER,
ROLE.GUEST,
],
creator=True,
model=Issue,
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], creator=True, model=Issue
)
def retrieve(self, request, slug, project_id, pk):
inbox_id = Inbox.objects.filter(
intake_id = Intake.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
project = Project.objects.get(pk=project_id)
inbox_issue = (
InboxIssue.objects.select_related("issue")
.prefetch_related(
"issue__labels",
"issue__assignees",
)
intake_issue = (
IntakeIssue.objects.select_related("issue")
.prefetch_related("issue__labels", "issue__assignees")
.annotate(
label_ids=Coalesce(
ArrayAgg(
"issue__labels__id",
distinct=True,
filter=~Q(issue__labels__id__isnull=True),
filter=Q(
~Q(issue__labels__id__isnull=True)
& Q(issue__label_issue__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -568,12 +529,15 @@ class InboxIssueViewSet(BaseViewSet):
ArrayAgg(
"issue__assignees__id",
distinct=True,
filter=~Q(issue__assignees__id__isnull=True),
filter=Q(
~Q(issue__assignees__id__isnull=True)
& Q(issue__issue_assignee__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
)
.get(inbox_id=inbox_id.id, issue_id=pk, project_id=project_id)
.get(intake_id=intake_id.id, issue_id=pk, project_id=project_id)
)
if (
ProjectMember.objects.filter(
@@ -584,37 +548,34 @@ class InboxIssueViewSet(BaseViewSet):
is_active=True,
).exists()
and not project.guest_view_all_features
and not inbox_issue.created_by == request.user
and not intake_issue.created_by == request.user
):
return Response(
{"error": "You are not allowed to view this issue"},
status=status.HTTP_400_BAD_REQUEST,
)
issue = InboxIssueDetailSerializer(inbox_issue).data
return Response(
issue,
status=status.HTTP_200_OK,
)
issue = IntakeIssueDetailSerializer(intake_issue).data
return Response(issue, status=status.HTTP_200_OK)
@allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=Issue)
def destroy(self, request, slug, project_id, pk):
inbox_id = Inbox.objects.filter(
intake_id = Intake.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
inbox_issue = InboxIssue.objects.get(
intake_issue = IntakeIssue.objects.get(
issue_id=pk,
workspace__slug=slug,
project_id=project_id,
inbox_id=inbox_id,
intake_id=intake_id,
)
# Check the issue status
if inbox_issue.status in [-2, -1, 0, 2]:
if intake_issue.status in [-2, -1, 0, 2]:
# Delete the issue also
issue = Issue.objects.filter(
workspace__slug=slug, project_id=project_id, pk=pk
).first()
issue.delete()
inbox_issue.delete()
intake_issue.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
+7 -31
View File
@@ -2,10 +2,7 @@
from itertools import chain
# Django imports
from django.db.models import (
Prefetch,
Q,
)
from django.db.models import Prefetch, Q
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
@@ -15,35 +12,16 @@ from rest_framework import status
# Module imports
from .. import BaseAPIView
from plane.app.serializers import (
IssueActivitySerializer,
IssueCommentSerializer,
)
from plane.app.permissions import (
ProjectEntityPermission,
allow_permission,
ROLE,
)
from plane.db.models import (
IssueActivity,
IssueComment,
CommentReaction,
)
from plane.app.serializers import IssueActivitySerializer, IssueCommentSerializer
from plane.app.permissions import ProjectEntityPermission, allow_permission, ROLE
from plane.db.models import IssueActivity, IssueComment, CommentReaction
class IssueActivityEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
permission_classes = [ProjectEntityPermission]
@method_decorator(gzip_page)
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
ROLE.GUEST,
]
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def get(self, request, slug, project_id, issue_id):
filters = {}
if request.GET.get("created_at__gt", None) is not None:
@@ -79,9 +57,7 @@ class IssueActivityEndpoint(BaseAPIView):
)
)
)
issue_activities = IssueActivitySerializer(
issue_activities, many=True
).data
issue_activities = IssueActivitySerializer(issue_activities, many=True).data
issue_comments = IssueCommentSerializer(issue_comments, many=True).data
if request.GET.get("activity_type", None) == "issue-property":
+32 -75
View File
@@ -3,7 +3,7 @@ import json
# Django imports
from django.core.serializers.json import DjangoJSONEncoder
from django.db.models import F, Func, OuterRef, Q, Prefetch, Exists, Case, When
from django.db.models import F, Func, OuterRef, Q, Prefetch, Exists, Subquery
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
@@ -12,9 +12,7 @@ from django.views.decorators.gzip import gzip_page
from rest_framework import status
from rest_framework.response import Response
from plane.app.permissions import (
ProjectEntityPermission,
)
from plane.app.permissions import ProjectEntityPermission
from plane.app.serializers import (
IssueFlatSerializer,
IssueSerializer,
@@ -27,6 +25,7 @@ from plane.db.models import (
IssueLink,
IssueSubscriber,
IssueReaction,
CycleIssue,
)
from plane.utils.grouper import (
issue_group_values,
@@ -35,10 +34,7 @@ from plane.utils.grouper import (
)
from plane.utils.issue_filters import issue_filters
from plane.utils.order_queryset import order_issue_queryset
from plane.utils.paginator import (
GroupedOffsetPaginator,
SubGroupedOffsetPaginator,
)
from plane.utils.paginator import GroupedOffsetPaginator, SubGroupedOffsetPaginator
from plane.app.permissions import allow_permission, ROLE
from plane.utils.error_codes import ERROR_CODES
@@ -65,12 +61,10 @@ class IssueArchiveViewSet(BaseViewSet):
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
.annotate(
cycle_id=Case(
When(
issue_cycle__cycle__deleted_at__isnull=True,
then=F("issue_cycle__cycle_id"),
),
default=None,
cycle_id=Subquery(
CycleIssue.objects.filter(
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
)
)
.annotate(
@@ -89,9 +83,7 @@ class IssueArchiveViewSet(BaseViewSet):
.values("count")
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -99,12 +91,7 @@ class IssueArchiveViewSet(BaseViewSet):
)
@method_decorator(gzip_page)
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
]
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def list(self, request, slug, project_id):
filters = issue_filters(request.query_params, "GET")
show_sub_issues = request.GET.get("show_sub_issues", "true")
@@ -120,8 +107,7 @@ class IssueArchiveViewSet(BaseViewSet):
)
# Issue queryset
issue_queryset, order_by_param = order_issue_queryset(
issue_queryset=issue_queryset,
order_by_param=order_by_param,
issue_queryset=issue_queryset, order_by_param=order_by_param
)
# Group by
@@ -130,9 +116,7 @@ class IssueArchiveViewSet(BaseViewSet):
# issue queryset
issue_queryset = issue_queryset_grouper(
queryset=issue_queryset,
group_by=group_by,
sub_group_by=sub_group_by,
queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by
)
if group_by:
@@ -152,9 +136,7 @@ class IssueArchiveViewSet(BaseViewSet):
order_by=order_by_param,
queryset=issue_queryset,
on_results=lambda issues: issue_on_results(
group_by=group_by,
issues=issues,
sub_group_by=sub_group_by,
group_by=group_by, issues=issues, sub_group_by=sub_group_by
),
paginator_cls=SubGroupedOffsetPaginator,
group_by_fields=issue_group_values(
@@ -172,10 +154,10 @@ class IssueArchiveViewSet(BaseViewSet):
group_by_field_name=group_by,
sub_group_by_field_name=sub_group_by,
count_filter=Q(
Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1)
| Q(issue_inbox__status=2)
| Q(issue_inbox__isnull=True),
Q(issue_intake__status=1)
| Q(issue_intake__status=-1)
| Q(issue_intake__status=2)
| Q(issue_intake__isnull=True),
archived_at__isnull=True,
is_draft=False,
),
@@ -188,9 +170,7 @@ class IssueArchiveViewSet(BaseViewSet):
order_by=order_by_param,
queryset=issue_queryset,
on_results=lambda issues: issue_on_results(
group_by=group_by,
issues=issues,
sub_group_by=sub_group_by,
group_by=group_by, issues=issues, sub_group_by=sub_group_by
),
paginator_cls=GroupedOffsetPaginator,
group_by_fields=issue_group_values(
@@ -201,10 +181,10 @@ class IssueArchiveViewSet(BaseViewSet):
),
group_by_field_name=group_by,
count_filter=Q(
Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1)
| Q(issue_inbox__status=2)
| Q(issue_inbox__isnull=True),
Q(issue_intake__status=1)
| Q(issue_intake__status=-1)
| Q(issue_intake__status=2)
| Q(issue_intake__isnull=True),
archived_at__isnull=True,
is_draft=False,
),
@@ -220,12 +200,7 @@ class IssueArchiveViewSet(BaseViewSet):
),
)
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
]
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def retrieve(self, request, slug, project_id, pk=None):
issue = (
self.get_queryset()
@@ -233,9 +208,7 @@ class IssueArchiveViewSet(BaseViewSet):
.prefetch_related(
Prefetch(
"issue_reactions",
queryset=IssueReaction.objects.select_related(
"issue", "actor"
),
queryset=IssueReaction.objects.select_related("issue", "actor"),
)
)
.prefetch_related(
@@ -266,24 +239,17 @@ class IssueArchiveViewSet(BaseViewSet):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def archive(self, request, slug, project_id, pk=None):
issue = Issue.issue_objects.get(
workspace__slug=slug,
project_id=project_id,
pk=pk,
workspace__slug=slug, project_id=project_id, pk=pk
)
if issue.state.group not in ["completed", "cancelled"]:
return Response(
{
"error": "Can only archive completed or cancelled state group issue"
},
{"error": "Can only archive completed or cancelled state group issue"},
status=status.HTTP_400_BAD_REQUEST,
)
issue_activity.delay(
type="issue.activity.updated",
requested_data=json.dumps(
{
"archived_at": str(timezone.now().date()),
"automation": False,
}
{"archived_at": str(timezone.now().date()), "automation": False}
),
actor_id=str(request.user.id),
issue_id=str(issue.id),
@@ -330,9 +296,7 @@ class IssueArchiveViewSet(BaseViewSet):
class BulkArchiveIssuesEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
permission_classes = [ProjectEntityPermission]
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def post(self, request, slug, project_id):
@@ -340,8 +304,7 @@ class BulkArchiveIssuesEndpoint(BaseAPIView):
if not len(issue_ids):
return Response(
{"error": "Issue IDs are required"},
status=status.HTTP_400_BAD_REQUEST,
{"error": "Issue IDs are required"}, status=status.HTTP_400_BAD_REQUEST
)
issues = Issue.objects.filter(
@@ -352,9 +315,7 @@ class BulkArchiveIssuesEndpoint(BaseAPIView):
if issue.state.group not in ["completed", "cancelled"]:
return Response(
{
"error_code": ERROR_CODES[
"INVALID_ARCHIVE_STATE_GROUP"
],
"error_code": ERROR_CODES["INVALID_ARCHIVE_STATE_GROUP"],
"error_message": "INVALID_ARCHIVE_STATE_GROUP",
},
status=status.HTTP_400_BAD_REQUEST,
@@ -362,10 +323,7 @@ class BulkArchiveIssuesEndpoint(BaseAPIView):
issue_activity.delay(
type="issue.activity.updated",
requested_data=json.dumps(
{
"archived_at": str(timezone.now().date()),
"automation": False,
}
{"archived_at": str(timezone.now().date()), "automation": False}
),
actor_id=str(request.user.id),
issue_id=str(issue.id),
@@ -382,6 +340,5 @@ class BulkArchiveIssuesEndpoint(BaseAPIView):
Issue.objects.bulk_update(bulk_archive_issues, ["archived_at"])
return Response(
{"archived_at": str(timezone.now().date())},
status=status.HTTP_200_OK,
{"archived_at": str(timezone.now().date())}, status=status.HTTP_200_OK
)
+10 -46
View File
@@ -45,10 +45,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
serializer.data,
cls=DjangoJSONEncoder,
),
current_instance=json.dumps(serializer.data, cls=DjangoJSONEncoder),
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
@@ -75,13 +72,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
return Response(status=status.HTTP_204_NO_CONTENT)
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
ROLE.GUEST,
]
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def get(self, request, slug, project_id, issue_id):
issue_attachments = FileAsset.objects.filter(
issue_id=issue_id, workspace__slug=slug, project_id=project_id
@@ -91,7 +82,6 @@ class IssueAttachmentEndpoint(BaseAPIView):
class IssueAttachmentV2Endpoint(BaseAPIView):
serializer_class = IssueAttachmentSerializer
model = FileAsset
@@ -103,10 +93,7 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
if not type or type not in settings.ATTACHMENT_MIME_TYPES:
return Response(
{
"error": "Invalid file type.",
"status": False,
},
{"error": "Invalid file type.", "status": False},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -121,11 +108,7 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
# Create a File Asset
asset = FileAsset.objects.create(
attributes={
"name": name,
"type": type,
"size": size_limit,
},
attributes={"name": name, "type": type, "size": size_limit},
asset=asset_key,
size=size_limit,
workspace_id=workspace.id,
@@ -139,9 +122,7 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
storage = S3Storage(request=request)
# Generate a presigned URL to share an S3 object
presigned_url = storage.generate_presigned_post(
object_name=asset_key,
file_type=type,
file_size=size_limit,
object_name=asset_key, file_type=type, file_size=size_limit
)
# Return the presigned URL
return Response(
@@ -177,13 +158,7 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
return Response(status=status.HTTP_204_NO_CONTENT)
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
ROLE.GUEST,
]
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def get(self, request, slug, project_id, issue_id, pk=None):
if pk:
# Get the asset
@@ -194,10 +169,7 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
# Check if the asset is uploaded
if not asset.is_uploaded:
return Response(
{
"error": "The asset is not uploaded.",
"status": False,
},
{"error": "The asset is not uploaded.", "status": False},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -221,13 +193,7 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
serializer = IssueAttachmentSerializer(issue_attachments, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
ROLE.GUEST,
]
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def patch(self, request, slug, project_id, issue_id, pk):
issue_attachment = FileAsset.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id
@@ -242,10 +208,7 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
serializer.data,
cls=DjangoJSONEncoder,
),
current_instance=json.dumps(serializer.data, cls=DjangoJSONEncoder),
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
@@ -253,6 +216,7 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
# Update the attachment
issue_attachment.is_uploaded = True
issue_attachment.created_by = request.user
# Get the storage metadata
if not issue_attachment.storage_metadata:

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