Compare commits

..
Author SHA1 Message Date
sriramveeraghanta d2110d3318 chore: api lint errors 2025-09-16 20:20:34 +05:30
c3e7cfd16b [WEB-4723] fix: disable project features on project create (#7625)
* fix: disbale project features on project create

* Implement migration 0105 to alter project cycle view fields to Boolean with default values

* Add project view settings in workspace seed task

* Add is_current_version_deprecated field to Instance model

Index user_id field in Session model

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2025-09-12 13:01:03 +05:30
Jayash TripathyandGitHub 9ffc30f7b1 [WEB-4889] refactor: add fill in bar chart bar stroke (#7776)
* ♻️ refactor: add fill in barchart bar stroke

* ♻️ refactor: added fill in the circle
2025-09-12 00:04:16 +05:30
Dheeraj Kumar KetireddyandGitHub b60f12a88e [WEB-4861] fix: update redirection path in MagicSignInEndpoint to home page (#7774)
* fix: update redirection path in MagicSignInEndpoint to home page

* Use / for clarity
2025-09-11 18:56:32 +05:30
Vamsi KrishnaandGitHub 76a0b38dd1 [WEB-4890]fix: dropdown width #7778 2025-09-11 18:56:04 +05:30
SangeethaandGitHub 8ee665f491 [WEB-4875] fix: unsubscribed work items on workspace subscribed work item filter #7775 2025-09-11 17:52:51 +05:30
Vamsi KrishnaandGitHub 85f23b450d [WEB-4852] chore: views refactor (#7729)
* chore: refactored view store and services

* chore: removed unused import

* chore: refactored update view component

* fix: lint errors
2025-09-11 17:09:56 +05:30
Vamsi KrishnaandGitHub 8bf059535a [WEB-4858]chore: updated content for error page (#7766)
* chore: updated content for error page

* chore: updated btn url
2025-09-11 14:22:46 +05:30
Vamsi KrishnaandGitHub 4cfea87108 [WEB-4857] fix: applied filters root update #7750 2025-09-11 14:21:30 +05:30
Vamsi KrishnaandGitHub 4fe2ef706b [WEB-4441]fix: members account type dropdown position #7759 2025-09-11 14:20:10 +05:30
sriram veeraghantaGitHubCopilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
8d354b3eb2 Potential fix for code scanning alert no. 636: URL redirection from remote source (#7760)
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-09-11 14:18:13 +05:30
SangeethaandGitHub ec541c2557 [WEB-4854] chore: project admin accesss to workspace admins (#7749)
* chore: project admin accesss to workspace admins

* chore: frontend changes

* chore: remove console.log

* chore: refactor permission decorator

* chore: role enum

* chore: rearrange role_choices
2025-09-11 14:16:36 +05:30
11cd8d11e4 [WIKI-632] chore: accept additional props for document collaborative editor (#7718)
* chore: add collaborative document editor extended props

* fix: additional rich text extension props

* fix: formatting

* chore: add types to the trailing node extension

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2025-09-11 14:15:39 +05:30
Jayash TripathyandGitHub 0f7bfdde91 [WEB-4877] fix: webapp crash because of bar chart (#7763)
* 🔧 fix: dynamic bar color handling and refactored color retrieval logic.

* ♻️  refactor: updated any to Record in getBarColor
2025-09-10 17:44:52 +05:30
Vamsi KrishnaandGitHub ac835bf287 [WEB-4874]fix: calendar picker build errors and styles override (#7762)
* fix: calender picker build errors

* fix: styles override in the picker implementation
2025-09-10 17:10:23 +05:30
sriram veeraghanta db18c3555c Merge branch 'canary' of github.com:makeplane/plane into preview 2025-09-10 14:45:14 +05:30
sriram veeraghantaandCopilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> b696ae91ed Potential fix for code scanning alert no. 646: Server-side request forgery (#7758)
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-09-10 14:44:34 +05:30
sriramveeraghanta 61e91bd09c chore (deps): django and vite version bump 2025-09-10 14:44:34 +05:30
sriram veeraghantaGitHubCopilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
20d773042b Potential fix for code scanning alert no. 646: Server-side request forgery (#7758)
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-09-10 14:42:46 +05:30
sriramveeraghanta 30b175108b chore (deps): django and vite version bump 2025-09-10 13:38:30 +05:30
6d116beea3 [WEB-4320] dev: propel emoji reaction component (#7741)
* dev: animated counter added to propel

* chore: animated counter story added

* chore: propel config updated

* chore: code refactor

* dev: emoji reaction and renderer component added to propel

* dev: emoji reaction story added

* chore: propel config updated

* chore: code refactor

* fix: format error

* chore: lint error resolved

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2025-09-10 00:54:47 +05:30
b0db4fcf10 [WEB-4736] dev: propel button (#7746)
* dev: button added to propel

* dev: button story added

* chore: propel config updated

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2025-09-10 00:34:17 +05:30
Anmol Singh BhatiaandGitHub 7e03264758 [WEB-4730] dev: propel context menu component (#7745)
* dev: context menu component added

* dev: context menu story added

* chore: propel config updated
2025-09-10 00:15:55 +05:30
Anmol Singh BhatiaandGitHub 1c8ac3d247 [WEB-4737] dev: propel pill component (#7743)
* dev: pill component added to propel

* dev: pill story added

* chore: propel config updated

* chore: code refactor
2025-09-10 00:15:37 +05:30
3b8bb1effc [WEB-4733] dev: propel toolbar component (#7742)
* dev: toolbar component added to propel

* dev: toolbar story added

* chore: propel config updated

* chore: code refactor

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2025-09-10 00:15:10 +05:30
Anmol Singh BhatiaandGitHub 5a63e6dad2 [WEB-4858] chore: error page ui revamp #7747 2025-09-10 00:03:39 +05:30
Anmol Singh BhatiaandGitHub 45688bdc72 [WEB-4860] dev: propel animated counter component (#7740)
* dev: animated counter added to propel

* chore: animated counter story added

* chore: propel config updated

* chore: code refactor

* chore: code refactor

* fix: format error
2025-09-09 23:51:45 +05:30
Jayash TripathyandGitHub 43b7a6ad0a [WEB-4855] refactor: chart tick improvements (#7732)
* 🚧 WIP: Introduced customTicks prop in BarChart for flexible tick rendering.

*  feat: added customTicks to axis charts for flexible tick rendering

* 🔧 fix: update default bar fill color to black and ensure consistent color usage in BarChart

*  feat: add customTooltipContent prop to LineChart for enhanced tooltip flexibility

* 🔧 fix: update bar fill color handling to support dynamic colors based on data and removed DEFAULT_BAR_FILL_COLOR

* 🔧 fix: correct bar fill color handling in BarChart to ensure proper color assignment for tooltips

* 🔧 fix: update customTicks prop types in TAxisChartProps to use unknown type for better type safety

* 📝 chore: updated translations and cleaned up insight card

* 🚨 fix: lint

* 🔧 fix: remove unused translation key "no_of" from Russian translations
2025-09-09 23:51:13 +05:30
Anmol Singh BhatiaandGitHub 498613284e [WEB-4841] chore: calendar component migration UI to propel (#7730)
* chore: move calendar components and dependencies

* chore: update package configurations

* chore: calendar import updated

* chore: propel config updated

* chore: propel calendar code refactor

* chore: code refactor

* fix: build error
2025-09-09 23:50:51 +05:30
Anmol Singh BhatiaandGitHub 9ab3143a73 [WEB-4816] chore: add label flow (#7716)
* chore: remove create label modal

* fix: label spinner

* chore: add label flow improvements

* chore: code refactor

* chore: code refactor

* chore: code refactor
2025-09-09 23:50:39 +05:30
56cd0fc445 [WEB-4025] fix: external user comment and reaction (#7692)
* chore: reactions types updated

* fix: external user comments

* fix: external user reactions

* chore: added display name for actor

* chore: merge conflicts

* chore: updated the created_by and updated_by

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2025-09-09 23:50:11 +05:30
Vamsi KrishnaandGitHub 260d9a053d [WEB-4802] fix: module sort order (#7674)
* fix: module sort order

* chore: removed secondary sort for progress

* chore: sort refactor
2025-09-09 23:49:30 +05:30
Vipin ChaudharyandGitHub 34bdc2ad76 [WIKI-636] fix: replace project loader with page loader #7752 2025-09-09 19:27:00 +05:30
99bc4262c5 [WIKI-654] fix: version history to load from html (#7754)
* fix: versions history to load from html

* chore: added json and stripped to store in version history (#7755)

---------

Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com>
2025-09-09 19:26:19 +05:30
Aaryan KhandelwalandGitHub 49127ebeea [WIKI-653] fix: page content loader #7753 2025-09-09 18:20:31 +05:30
Vamsi KrishnaandGitHub a8a6536379 [WEB-4688]feat: added collapsible to propel (#7643)
* chore: added collapsibl to propel

* fix: export path

* chore: made collapsible a compound component

* fix: lint and format errors

* chore: updated propel exports order and added collapsible to tsdown config
2025-09-09 13:59:00 +05:30
Vamsi KrishnaandGitHub 11b83cf4f2 [WEB-4692]feat: added toast to propel package (#7640)
* * chore: migrated toast to base ui toast
* chore: added toast to propel package

* fix: lint errors

* fix: export path

* fix: lint errors

* chore: imporoved props handling

* fix: lint errors

* chore: updated import paths

* chore: replaced clsx with cn

* * chore: updated tsdown config
* chore: updated exports order
2025-09-09 13:58:11 +05:30
Anmol Singh BhatiaandGitHub fbd48c33f5 [WEB-4729] dev: propel scrollarea (#7748)
* dev: scrollarea added to propel

* dev: scrollarea story added

* chore: propel config updated

* fix: format error
2025-09-09 13:56:28 +05:30
Vamsi KrishnaandGitHub 37ce8a9fe6 [WEB-4800]chore: settings header component refactor #7669 2025-09-09 13:42:40 +05:30
Bavisetti NarayanandGitHub 291101a8e5 chore: added attributes in ol tag (#7736) 2025-09-08 17:17:38 +05:30
Vipin ChaudharyandGitHub 0cb4976e38 [WIKI-647] chore: update image path (#7734)
* chore: update image path

* chore: update asset path
2025-09-08 15:44:56 +05:30
Anmol Singh BhatiaandGitHub 20d139cc9e [WEB-4839] chore: maintenance mode empty state updated (#7728)
* chore: maintenance mode empty state updated

* chore: code refactor

* chore: code refactor

* chore: code refactor
2025-09-05 20:16:39 +05:30
ff181e566f [WEB-4738] feat: add Skeleton component with respective stories in propel (#7704)
*  feat: add Skeleton component with respective stories in propel

* 🚨 fix: lint issues

* ♻️ refactor : enhance Skeleton component with improved props and structure

*  feat: add Skeleton component entry to package.json

* 🚨 fix: lint

* ♻️ refactor: rename Skeleton to skeleton

* fix: rename fixes

* ♻️ refactor: rename skeleton

* 🚨 fix: lint

* ♻️ refactor: add skeleton entry to tsdown config and package.json

* ♻️  refactor: sorted propel exports

---------

Co-authored-by: sriramveeraghanta <veeraghanta.sriram@gmail.com>
2025-09-05 15:51:05 +05:30
Jayash TripathyandGitHub a696b6039c [WEB-4835] feat: storybook enhancements (#7702)
*  feat: integrate Storybook addons and enhance Tabs component

- Added `@storybook/addon-designs` and `@storybook/addon-docs` to the project dependencies.
- Enhanced the Tabs component stories to support dynamic tab options.
- Introduced a custom theme for Storybook with a new manager configuration.
- Added a new SVG logo for branding in Storybook.
- Pinned the storybook and related packages version

* ♻️  refactor: update Storybook manager configuration by removing unused import

* ♻️  refactor: enhance Tabs story with size labels and layout adjustments

* 🚨 fix: lint issues

* 🚨 fix: lock file

* ✏️  fix: update brand image path in Storybook manager configuration

* ♻️ refactor: improve Tabs story by ensuring safe default value and enhancing content rendering
2025-09-05 14:55:33 +05:30
Vipin ChaudharyandGitHub af1dcd335e [WIKI-640] chore: refactor link view #7727 2025-09-05 14:14:30 +05:30
Aaryan KhandelwalandGitHub a1500c2206 [WIKI-644] fix: remove unnecessary props from the editor package #7726 2025-09-05 13:50:44 +05:30
Aaryan KhandelwalandGitHub 88f194ca8e [WIKI-637] fix: work item description version history (#7725)
* fix: work item description version history

* chore: add key to the editor
2025-09-04 18:57:50 +05:30
Aaryan KhandelwalandGitHub b68d2ca921 [WIKI-642] fix: editor bubble menu overflow issues #7724 2025-09-04 18:16:03 +05:30
Aaryan KhandelwalandGitHub 78c6aede52 [WIKI-641] fix: overwrite tailwind typography plugin styles #7723 2025-09-04 18:14:48 +05:30
sriramveeraghanta c6c46f9aab Merge branch 'canary' of github.com:makeplane/plane into preview 2025-09-04 14:48:27 +05:30
Anmol Singh BhatiaandGitHub 7d7e37439d [WEB-4840] chore: icon migration from @plane/ui to @plane/propel/icons (#7721)
* chore: move icons from ui package to propel package

* chore: package and tsdown config updated

* chore: migrate all icon imports from @plane/ui to @plane/propel/icons

* chore: remove icon components from @plane/ui package (migrated to
  @plane/propel/icons)

* chore: code refactoring

* chore: migrate remaining icon components from @Plane/ui to @Plane/propel/icons

* fix: lint error

* chore: code refactor
2025-09-04 14:44:53 +05:30
Akshat JainandGitHub d258080ee7 [INFRA-235] fix: docker compose file restart_policy changed to any (#7689)
* chore: update docker-compose.yml to change restart policy condition from 'on-failure' to 'any' and remove SSL variable from variables.env

* fix: update docker-compose.yml to change restart policy condition from 'any' to 'on-failure'
2025-09-04 14:18:45 +05:30
Bavisetti Narayanandsriramveeraghanta c40c7804e7 [WEB-4806] chore: updated html attribute validation #7719 2025-09-04 13:53:21 +05:30
Bavisetti NarayanandGitHub 36c735bede [WEB-4806] chore: updated html attribute validation #7719 2025-09-04 13:52:43 +05:30
Bavisetti Narayanandsriramveeraghanta cab5d20217 [WEB-4806] chore: improved the html validation (#7676)
* chore: improved the html validation

* chore: removed the models changes

* chore: removed extra filters

* chore: changed the protocols
2025-09-03 19:57:37 +05:30
Bavisetti NarayanandGitHub 9965fc75a6 [WEB-4806] chore: improved the html validation (#7676)
* chore: improved the html validation

* chore: removed the models changes

* chore: removed extra filters

* chore: changed the protocols
2025-09-03 19:56:20 +05:30
Dheeraj Kumar KetireddyandGitHub cb1e93f83d [WEB-4831] schema: add rich_filters field to multiple models for enhanced filtering capabilities (#7700)
* chore: add rich_filters field to multiple models for enhanced filtering capabilities

* Set the default  value to empty dict
2025-09-03 18:37:56 +05:30
SangeethaandGitHub 2e673026d4 [WEB-4129] fix: work item filter assignees missing fields #7698 2025-09-03 18:36:04 +05:30
NikhilandGitHub 49a40031a4 [WEB-4795] chore: remove caching decorator from WorkspaceStatesEndpoint #7714 2025-09-03 18:28:11 +05:30
54e4a0f6dd fix: build error due to exports in propel package (#7715)
* fix: build error due to exports in propel package

* fix: editor css exports

* fix: editor css build

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2025-09-03 18:11:31 +05:30
sriram veeraghantaandGitHub 473535fcfa [WEB-4844] fix: export prople package dist (#7712)
* fix: export prople package dist

* fix: code refactor
2025-09-03 17:25:31 +05:30
Akshita GoyalandGitHub 59ace4f411 [WEB-4787] fix: changed issue to work item in cycles dropdown (#7706) 2025-09-03 14:11:30 +05:30
b99ddc24e7 [WEB-4810] feat: migrate to tsdown from tsup (#7679)
* feat: migrat to tsdown to tsup

* fix: build scripts

* fix: lock file fixes

* fix: adding build process to i18n and propel packages

* fix: lint warning

* chore: update services module entry points

* fix: lock file

* fix: lock file

* fix: remove tsc from build

* fix: tsdown configs

* fix: remove tsc step from build process

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
Co-authored-by: Aaron Reisman <aaron.reisman@plane.so>
2025-09-03 14:01:57 +05:30
Anmol Singh BhatiaandGitHub 91f0228b5f [WEB-4838] fix: github logo theme #7709 2025-09-02 22:39:42 +05:30
sriram veeraghantaandGitHub 4b74751e4e [WEB-4837] feat: adding more packages to catalog (#7693)
* feat: adding more packages to catalog

* fix: lock file

* fix: sharp pacakge catalog
2025-09-02 21:03:03 +05:30
Anmol Singh BhatiaandGitHub 1c28bd5887 [WEB-4724] fix: editor tooltip (#7708)
* fix: editor tooltip code refactoring

* chore: code refactor
2025-09-02 20:40:14 +05:30
Aaryan KhandelwalandGitHub 292264ba96 [WIKI-574] fix: disable copy comment link option for intake work item #7707 2025-09-02 20:39:09 +05:30
Prateek ShouryaandGitHub 28d368d502 [WEB-4836] fix: work item retrieval in peek overview (#7705) 2025-09-02 19:26:35 +05:30
Anmol Singh BhatiaandGitHub 652a6cc885 [WEB-4734] feat: replace emoji picker with frimousse (#7639) 2025-09-02 19:00:15 +05:30
26b48bfcf0 [WEB-4724] feat: migrate tooltips from blueprintjs to base-ui (#7646)
* feat: add card component to propel package and update tooltip imports

* refactor: remove @plane/ui dependency and update tooltip imports to use local card component

* fix: lint

* refactor: update import from @plane/ui to @plane/utils in command component

* chore: removed blueprintjs/core and blueprintjs/popover2 dependencies

* chore: updated tooltip instances across platform and performed related code refactoring

* chore: code refactor

* chore: code refactor

* fix: lint and build error

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: code refactor

* fix: format issue

* fix: build fix

---------

Co-authored-by: Jayash Tripathy <76092296+JayashTripathy@users.noreply.github.com>
2025-09-02 18:19:56 +05:30
Vamsi KrishnaandGitHub 569b2fba83 [WEB-4196]fix: toast message for sub work item quick actions (#7688)
* fix: toast message for sub work item quick actions

* chore: updated translation files
2025-09-02 18:02:30 +05:30
Anmol Singh BhatiaandGitHub 779a96acb8 [WEB-4834] fix: range date picker weekStartsOn (#7699)
* fix: range date picker weekStartsOn

* chore: code refactor
2025-09-02 18:02:09 +05:30
guru_sainathandGitHub 2d31b562d8 [WEB-4823] chore: Add compound indexing for notification fields to improve query performance (#7691)
* dev: handled indexing for the notification fields

* dev: removing indexing related to workspaces

* dev: handled indexing for file asset, user favorite, and page log

* dev: indexing concurrently
2025-09-02 14:18:13 +05:30
50b4aca726 [WEB-4829] refactor: star us on GitHub link component (#7697)
* [WEB-4829] refactor: star us on github link

* fix: review comment

---------

Co-authored-by: sriramveeraghanta <veeraghanta.sriram@gmail.com>
2025-09-02 13:54:13 +05:30
Prateek ShouryaandGitHub d960d7ce88 [WEB-4828] refactor: remove legacy project-level draft work items components (#7694) 2025-09-02 12:29:00 +05:30
M. PalanikannanandGitHub fd5ba6c7b8 [WIKI-511] [WIKI-572] fix: extended page root and editor body (#7661)
* fix: better refactoring of page root and editor body

* fix: editor sideeffects

* fix: add comments json field

* fix: props name

* fix: review changes

* fix: extra checks

* fix: type changes

* fix: remove editor ref current

* fix: renaming

* fix: link check
2025-09-01 22:17:53 +05:30
f42eeec2c0 [WEB-4686] feat: propel tabs (#7620)
* chore: global css file added to tailwind config package

* chore: tailwind config updated

* chore: cn utility function added to propel package

* chore: storybook init

* fix: format error

* feat: added base ui tabs

* fix: add missing newline at end of package.json in propel package

* fix: reorder import statement for Tabs component in propel package

* feat: refactor Tabs component to support compound structure with forward refs

* fix: lint

* chore: code refactor

* chore: code refactor

* fix: lock file

* chore: added stories for tabs

* refactor: clean up

* fix: lint

* fix: lint

* fix: Remove duplicate storybook ESLint config

* fix: lint

* fix: update classname import path in Tabs component

---------

Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
Co-authored-by: sriramveeraghanta <veeraghanta.sriram@gmail.com>
2025-09-01 19:58:40 +05:30
Vamsi KrishnaandGitHub e679dc3d12 [WEB-4814] chore: enabled reordering in list when group is none #7684 2025-09-01 19:46:17 +05:30
Jayash TripathyandGitHub f2edf637de [WEB-4809] chore: added common libs to pnpm workspace catalog (#7611)
* chore: added common libs to pnpm workspace catalog

* chore: update pnpm lockfile to use exact versions for React and TypeScript dependencies

* refactor: removed string from the versions

* fix: lint

* refactor: cleanup

* fix: lint

* chore: updated lock file
2025-09-01 19:42:56 +05:30
Vamsi KrishnaandGitHub 7437deaa86 [WEB-4689]chore: added accordion to propel (#7641)
* chore: added accordion to propel

* fix: lint errors

* fix: updated export path

* fix: lint errors

* chore: made accordion into compound component

* fix: coderabbit suggestions
2025-09-01 19:42:12 +05:30
64b95daff4 [WEB-4740] feat: add propel seperator (#7637)
* chore: global css file added to tailwind config package

* chore: tailwind config updated

* chore: cn utility function added to propel package

* chore: storybook init

* fix: format error

* chore: code refactor

* chore: code refactor

* fix: format error

* feat: add propel seperator component

* 🔒 chore: updated lock file

* ✏️ fix: typo in separator filename and some linting issues

* ♻️ refactor: replace clsx with cn utility in Separator component for class name management

* 🐛 fix: re-added twMerge

* 🧹 cleanup: remove unnecessary blank line in Separator component

---------

Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
2025-09-01 19:40:01 +05:30
sriramveeraghanta 34181fba80 chore: version bump 2025-09-01 19:38:21 +05:30
Vipin ChaudharyandGitHub 0358e9b965 [WIKI-589] fix: update project page creation flow (#7685)
* fix : update project page creation flow

* chore: update translations

* chore: remove unused changes
2025-09-01 16:43:07 +05:30
Vipin ChaudharyandGitHub ee471c772a [WIKI-622] fix: update space work item description styles #7686 2025-09-01 15:13:52 +05:30
e2c0d0f23c [WEB-4813] fix: ensure all identifiers in log transformations are converted to strings (#7682)
* fix: ensure all identifiers in log transformations are converted to strings

* Update apps/api/plane/bgtasks/cleanup_task.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-01 14:12:34 +05:30
Anmol Singh BhatiaandGitHub 3ecebc02ae [WEB-4811] chore: plane logo asset updated #7683 2025-09-01 13:52:05 +05:30
Anmol Singh BhatiaandGitHub 16d531cc7a [WEB-4808] fix: joinUrlPath utility fn #7678 2025-09-01 13:51:26 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ab283c7c78 chore(deps): bump the npm_and_yarn group across 4 directories with 1 update (#7681)
Bumps the npm_and_yarn group with 1 update in the / directory: [next](https://github.com/vercel/next.js).
Bumps the npm_and_yarn group with 1 update in the /apps/admin directory: [next](https://github.com/vercel/next.js).
Bumps the npm_and_yarn group with 1 update in the /apps/space directory: [next](https://github.com/vercel/next.js).
Bumps the npm_and_yarn group with 1 update in the /apps/web directory: [next](https://github.com/vercel/next.js).


Updates `next` from 14.2.30 to 14.2.32
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v14.2.30...v14.2.32)

Updates `next` from 14.2.30 to 14.2.32
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v14.2.30...v14.2.32)

Updates `next` from 14.2.30 to 14.2.32
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v14.2.30...v14.2.32)

Updates `next` from 14.2.30 to 14.2.32
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v14.2.30...v14.2.32)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 14.2.32
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: next
  dependency-version: 14.2.32
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: next
  dependency-version: 14.2.32
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: next
  dependency-version: 14.2.32
  dependency-type: direct:production
  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>
2025-09-01 13:05:18 +05:30
Anmol Singh BhatiaandGitHub 7c1bbf4a6f [WEB-4807] fix: workspace menu item highlight #7677 2025-08-29 20:23:31 +05:30
sriram veeraghantaandGitHub 4293892178 [WEB-4803] fix: eslint errors in code editors (#7675)
* fix: eslint errors in code editors

* fix: removed workspaces from package.json
2025-08-29 19:44:46 +05:30
Bavisetti NarayanandGitHub ebd517bb7d [WIKI-619] chore: added sort order migration for page model #7673 2025-08-29 19:44:29 +05:30
Henit ChobisaandGitHub 4042af9f32 feat: added support for expanding updated_by in work item (#7667)
* feat: added support for expanding `updated_by` and `type` in work item

* fix: moved type to dictionary for expansion

* fix: refactored unnecessary fields
2025-08-29 16:41:54 +05:30
258d24bf06 [SILO-454] chore: refactor decorator, logger packages (#7618)
* [SILO-454] chore: refactor decorator, logger packages

- add registerControllers function abstracting both rest, ws controllers
- update logger to a simple json based logger

* fix: logger instance and middleware

* fix: type and module resolutions

* fix: lodash type package update

* fix: bypass lint errors in decorators

* chore: format changes

---------

Co-authored-by: sriramveeraghanta <veeraghanta.sriram@gmail.com>
2025-08-29 14:29:16 +05:30
NikhilandGitHub 489a6e1e94 [WEB-4796] fix: update MongoDB collection check to use 'is not None' for better clarity #7671 2025-08-29 14:25:46 +05:30
sriram veeraghantaandGitHub 4f349807be chore: docker image builds by removing node version in workspace.yml file (#7672)
* fix: docker image builts by removing nodeversion in workspace.yml file

* chore: remve .nvmrc file
2025-08-29 14:25:07 +05:30
Vipin ChaudharyandGitHub 7a43137620 [WIKI-556] fix : invert tracking logic #7668 2025-08-28 20:37:20 +05:30
e144ce8cf2 [WIKI-556] chore: disable tracking of page hover (#7650)
* chore: disable tracking of page hover

* chore: add track check for page feth

* chore: make track check mandatory

* chore: update track format

---------

Co-authored-by: VipinDevelops <vipinchaudhary1809@gmail.com>
2025-08-28 20:02:44 +05:30
Anmol Singh BhatiaandGitHub ba7303b7af [WEB-4559] fix: workspace menu item active state (#7666)
* fix: highlight active workspace menu item correctly

* chore: code refactor
2025-08-28 20:01:28 +05:30
Prateek ShouryaandGitHub e0912ccefc [WEB-4040] fix: minor changes in base plan names (#7656) 2025-08-28 18:51:10 +05:30
Anmol Singh BhatiaandGitHub aef465415b [WEB-4748] chore: propel utils (#7662)
* chore: tailwind merge added to propel

* chore: classname utils updated

* chore: utils import alias added

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: code refactor
2025-08-28 18:39:27 +05:30
Vamsi KrishnaandGitHub 3b3bd3e54e [WEB-4722] fix: fixed draft state update #7663 2025-08-28 18:38:30 +05:30
SangeethaandGitHub f2fabff10a [WEB-4521] fix: attachments #7665 2025-08-28 18:36:50 +05:30
NikhilandGitHub ddeabeeeb1 [WEB-4720] fix: mongo connection class to initialize mongo db #7652 2025-08-28 13:57:16 +05:30
sriram veeraghantaandGitHub 0e6fbaee3a [WEB-4790] fix: moved typescript parser to the base eslint config (#7658)
* fix: moved typescript parser to the base eslint config

* fix: eslint warning

* fix: type config setting

* fix: convert live eslint to cjs
2025-08-27 21:03:20 +05:30
Anmol Singh BhatiaandGitHub cfe710d492 [WEB-4779] chore: app sidebar wrapper component for consistency and reusability (#7655)
* chore: app sidebar ui enhancements and code refactor

* chore: code refactor

* chore: code refactor
2025-08-27 20:46:43 +05:30
AaronandGitHub 4a3c172992 feat(repo): add diff checks in CI flows (#7657) 2025-08-27 16:31:23 +05:30
Vipin ChaudharyandGitHub 5d1ad8a183 [WIKI-550] fix: emoji modal for touch device (#7651)
* fix: emoji modal for touch device

* refactor: editor from props

* fix : update is touch device plugin
2025-08-27 00:39:47 +05:30
Vipin ChaudharyandGitHub f95a07d8c8 [WIKI-436] fix: check for code in alignment #7649 2025-08-27 00:39:26 +05:30
Bavisetti NarayanandGitHub 0af75897f5 [WEB-4780] chore: changed the html validation (#7648)
* chore: changed the html validation

* chore: added requirements for nh3

* chore: removed the json validations
2025-08-27 00:38:25 +05:30
Vamsi KrishnaandGitHub 3602ff6930 [WEB-4781] fix: add peek view get to store #7654 2025-08-26 18:24:08 +05:30
Prateek ShouryaandGitHub 6d1275d58c [WEB-4761] fix: resolve circular import between IssueService and local db (#7653)
- Move local db import to dynamic import to break circular dependency
2025-08-26 16:48:32 +05:30
Vipin ChaudharyandGitHub cf7b288f93 [WIKI-345] regression: update block menu options (#7647)
* chore: update flagged and disabled extension

* chore :build fix
2025-08-26 14:18:26 +05:30
865 changed files with 18273 additions and 12427 deletions
+21 -12
View File
@@ -35,6 +35,10 @@ on:
- preview
- canary
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
TARGET_BRANCH: ${{ github.ref_name }}
ARM64_BUILD: ${{ github.event.inputs.arm64 }}
@@ -268,15 +272,14 @@ jobs:
if: ${{ needs.branch_build_setup.outputs.aio_build == 'true' }}
name: Build-Push AIO Docker Image
runs-on: ubuntu-22.04
needs: [
branch_build_setup,
branch_build_push_admin,
branch_build_push_web,
branch_build_push_space,
branch_build_push_live,
branch_build_push_api,
branch_build_push_proxy
]
needs:
- branch_build_setup
- branch_build_push_admin
- branch_build_push_web
- branch_build_push_space
- branch_build_push_live
- branch_build_push_api
- branch_build_push_proxy
steps:
- name: Checkout Files
uses: actions/checkout@v4
@@ -285,7 +288,7 @@ jobs:
id: prepare_aio_assets
run: |
cd deployments/aio/community
if [ "${{ needs.branch_build_setup.outputs.build_type }}" == "Release" ]; then
aio_version=${{ needs.branch_build_setup.outputs.release_version }}
else
@@ -324,7 +327,14 @@ jobs:
upload_build_assets:
name: Upload Build Assets
runs-on: ubuntu-22.04
needs: [branch_build_setup, branch_build_push_admin, branch_build_push_web, branch_build_push_space, branch_build_push_live, branch_build_push_api, branch_build_push_proxy]
needs:
- branch_build_setup
- branch_build_push_admin
- branch_build_push_web
- branch_build_push_space
- branch_build_push_live
- branch_build_push_api
- branch_build_push_proxy
steps:
- name: Checkout Files
uses: actions/checkout@v4
@@ -397,4 +407,3 @@ jobs:
${{ github.workspace }}/deployments/cli/community/docker-compose.yml
${{ github.workspace }}/deployments/cli/community/variables.env
${{ github.workspace }}/deployments/swarm/community/swarm.sh
-2
View File
@@ -17,8 +17,6 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Get PR Branch version
run: echo "PR_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV
@@ -3,11 +3,21 @@ name: Build and lint API
on:
workflow_dispatch:
pull_request:
branches: ["preview"]
types: ["opened", "synchronize", "ready_for_review", "review_requested", "reopened"]
branches:
- "preview"
types:
- "opened"
- "synchronize"
- "ready_for_review"
- "review_requested"
- "reopened"
paths:
- "apps/api/**"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint-api:
name: Lint API
@@ -3,21 +3,18 @@ name: Build and lint web apps
on:
workflow_dispatch:
pull_request:
branches: ["preview"]
branches:
- "preview"
types:
[
"opened",
"synchronize",
"ready_for_review",
"review_requested",
"reopened",
]
paths:
- "**.tsx?"
- "**.jsx?"
- "**.css"
- "**.json"
- "!apps/api/**"
- "opened"
- "synchronize"
- "ready_for_review"
- "review_requested"
- "reopened"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-and-lint:
@@ -27,16 +24,18 @@ jobs:
if: |
github.event.pull_request.draft == false &&
github.event.pull_request.requested_reviewers != null
env:
TURBO_SCM_BASE: ${{ github.event.pull_request.base.sha }}
TURBO_SCM_HEAD: ${{ github.sha }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 2
fetch-depth: 50
filter: blob:none
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- name: Enable Corepack and pnpm
run: corepack enable pnpm
@@ -44,11 +43,11 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint web apps
run: pnpm run check:lint
- name: Lint Affected
run: pnpm turbo run check:lint --affected
- name: Check format
run: pnpm run check:format
- name: Check Affected format
run: pnpm turbo run check:format --affected
- name: Build apps
run: pnpm run build
- name: Build Affected
run: pnpm turbo run build --affected
+1 -1
View File
@@ -14,7 +14,7 @@ strict-peer-dependencies=false
# Turbo occasionally performs postinstall tasks for optimal performance
# moved to pnpm-workspace.yaml: onlyBuiltDependencies (e.g., allow turbo)
public-hoist-pattern[]=eslint
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=prettier
public-hoist-pattern[]=typescript
-1
View File
@@ -1 +0,0 @@
lts/jod
+12
View File
@@ -0,0 +1,12 @@
.next/*
out/*
public/*
dist/*
node_modules/*
.turbo/*
.env*
.env
.env.local
.env.development
.env.production
.env.test
-1
View File
@@ -1,5 +1,4 @@
module.exports = {
root: true,
extends: ["@plane/eslint-config/next.js"],
parser: "@typescript-eslint/parser",
};
@@ -9,7 +9,7 @@ import { useInstance } from "@/hooks/store";
// components
import { InstanceEmailForm } from "./email-config-form";
const InstanceEmailPage = observer(() => {
const InstanceEmailPage: React.FC = observer(() => {
// store
const { fetchInstanceConfigurations, formattedConfig, disableEmail } = useInstance();
@@ -29,7 +29,7 @@ const InstanceEmailPage = observer(() => {
message: "Email feature has been disabled",
type: TOAST_TYPE.SUCCESS,
});
} catch (error) {
} catch (_error) {
setToast({
title: "Error disabling email",
message: "Failed to disable email feature. Please try again.",
@@ -7,7 +7,8 @@ import { ExternalLink, FileText, HelpCircle, MoveLeft } from "lucide-react";
import { Transition } from "@headlessui/react";
// plane internal packages
import { WEB_BASE_URL } from "@plane/constants";
import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui";
import { DiscordIcon, GithubIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import { cn } from "@plane/utils";
// hooks
import { useTheme } from "@/hooks/store";
@@ -5,7 +5,8 @@ import Link from "next/link";
import { usePathname } from "next/navigation";
import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
// plane internal packages
import { Tooltip, WorkspaceIcon } from "@plane/ui";
import { WorkspaceIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import { cn } from "@plane/utils";
// hooks
import { useTheme } from "@/hooks/store";
+1 -1
View File
@@ -1,7 +1,7 @@
"use client";
import Link from "next/link";
import { PlaneLockup } from "@plane/ui";
import { PlaneLockup } from "@plane/propel/icons";
export const AuthHeader = () => (
<div className="flex items-center justify-between gap-6 w-full flex-shrink-0 sticky top-0">
@@ -25,9 +25,8 @@ export const EmailCodesConfiguration: React.FC<Props> = observer((props) => {
<ToggleSwitch
value={Boolean(parseInt(enableMagicLogin))}
onChange={() => {
Boolean(parseInt(enableMagicLogin)) === true
? updateConfig("ENABLE_MAGIC_LINK_LOGIN", "0")
: updateConfig("ENABLE_MAGIC_LINK_LOGIN", "1");
const newEnableMagicLogin = Boolean(parseInt(enableMagicLogin)) === true ? "0" : "1";
updateConfig("ENABLE_MAGIC_LINK_LOGIN", newEnableMagicLogin);
}}
size="sm"
disabled={disabled}
@@ -35,9 +35,8 @@ export const GithubConfiguration: React.FC<Props> = observer((props) => {
<ToggleSwitch
value={Boolean(parseInt(enableGithubConfig))}
onChange={() => {
Boolean(parseInt(enableGithubConfig)) === true
? updateConfig("IS_GITHUB_ENABLED", "0")
: updateConfig("IS_GITHUB_ENABLED", "1");
const newEnableGithubConfig = Boolean(parseInt(enableGithubConfig)) === true ? "0" : "1";
updateConfig("IS_GITHUB_ENABLED", newEnableGithubConfig);
}}
size="sm"
disabled={disabled}
@@ -35,9 +35,8 @@ export const GitlabConfiguration: React.FC<Props> = observer((props) => {
<ToggleSwitch
value={Boolean(parseInt(enableGitlabConfig))}
onChange={() => {
Boolean(parseInt(enableGitlabConfig)) === true
? updateConfig("IS_GITLAB_ENABLED", "0")
: updateConfig("IS_GITLAB_ENABLED", "1");
const newEnableGitlabConfig = Boolean(parseInt(enableGitlabConfig)) === true ? "0" : "1";
updateConfig("IS_GITLAB_ENABLED", newEnableGitlabConfig);
}}
size="sm"
disabled={disabled}
@@ -35,9 +35,8 @@ export const GoogleConfiguration: React.FC<Props> = observer((props) => {
<ToggleSwitch
value={Boolean(parseInt(enableGoogleConfig))}
onChange={() => {
Boolean(parseInt(enableGoogleConfig)) === true
? updateConfig("IS_GOOGLE_ENABLED", "0")
: updateConfig("IS_GOOGLE_ENABLED", "1");
const newEnableGoogleConfig = Boolean(parseInt(enableGoogleConfig)) === true ? "0" : "1";
updateConfig("IS_GOOGLE_ENABLED", newEnableGoogleConfig);
}}
size="sm"
disabled={disabled}
@@ -25,9 +25,8 @@ export const PasswordLoginConfiguration: React.FC<Props> = observer((props) => {
<ToggleSwitch
value={Boolean(parseInt(enableEmailPassword))}
onChange={() => {
Boolean(parseInt(enableEmailPassword)) === true
? updateConfig("ENABLE_EMAIL_PASSWORD", "0")
: updateConfig("ENABLE_EMAIL_PASSWORD", "1");
const newEnableEmailPassword = Boolean(parseInt(enableEmailPassword)) === true ? "0" : "1";
updateConfig("ENABLE_EMAIL_PASSWORD", newEnableEmailPassword);
}}
size="sm"
disabled={disabled}
@@ -1,7 +1,7 @@
"use client";
import Link from "next/link";
import { Tooltip } from "@plane/ui";
import { Tooltip } from "@plane/propel/tooltip";
type Props = {
label?: string;
@@ -2,7 +2,7 @@ import { observer } from "mobx-react";
import { ExternalLink } from "lucide-react";
// plane internal packages
import { WEB_BASE_URL } from "@plane/constants";
import { Tooltip } from "@plane/ui";
import { Tooltip } from "@plane/propel/tooltip";
import { getFileURL } from "@plane/utils";
// hooks
import { useWorkspace } from "@/hooks/store";
+1 -1
View File
@@ -209,7 +209,7 @@ export class InstanceStore implements IInstanceStore {
});
});
await this.instanceService.disableEmail();
} catch (error) {
} catch (_error) {
console.error("Error disabling the email");
this.instanceConfigurations = instanceConfigurations;
}
+16 -16
View File
@@ -1,7 +1,7 @@
{
"name": "admin",
"description": "Admin UI for Plane",
"version": "0.28.0",
"version": "1.0.0",
"license": "AGPL-3.0",
"private": true,
"scripts": {
@@ -26,30 +26,30 @@
"@plane/ui": "workspace:*",
"@plane/utils": "workspace:*",
"autoprefixer": "10.4.14",
"axios": "1.11.0",
"lodash": "^4.17.21",
"lucide-react": "^0.469.0",
"mobx": "^6.12.0",
"mobx-react": "^9.1.1",
"next": "14.2.30",
"axios": "catalog:",
"lodash": "catalog:",
"lucide-react": "catalog:",
"mobx": "catalog:",
"mobx-react": "catalog:",
"next": "catalog:",
"next-themes": "^0.2.1",
"postcss": "^8.4.49",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react": "catalog:",
"react-dom": "catalog:",
"react-hook-form": "7.51.5",
"sharp": "^0.33.5",
"swr": "^2.2.4",
"uuid": "^9.0.1"
"sharp": "catalog:",
"swr": "catalog:",
"uuid": "catalog:"
},
"devDependencies": {
"@plane/eslint-config": "workspace:*",
"@plane/tailwind-config": "workspace:*",
"@plane/typescript-config": "workspace:*",
"@types/lodash": "^4.17.6",
"@types/lodash": "catalog:",
"@types/node": "18.16.1",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.2.18",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@types/uuid": "^9.0.8",
"typescript": "5.8.3"
"typescript": "catalog:"
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "plane-api",
"version": "0.28.0",
"version": "1.0.0",
"license": "AGPL-3.0",
"private": true,
"description": "API server powering Plane's backend"
+3 -1
View File
@@ -29,7 +29,8 @@ class BaseSerializer(serializers.ModelSerializer):
"""
Adjust the serializer's fields based on the provided 'fields' list.
:param fields: List or dictionary specifying which fields to include in the serializer.
:param fields: List or dictionary specifying which fields to include
in the serializer.
:return: The updated fields for the serializer.
"""
# Check each field_name in the provided fields.
@@ -91,6 +92,7 @@ class BaseSerializer(serializers.ModelSerializer):
"project_lead": UserLiteSerializer,
"state": StateLiteSerializer,
"created_by": UserLiteSerializer,
"updated_by": UserLiteSerializer,
"issue": IssueSerializer,
"actor": UserLiteSerializer,
"owned_by": UserLiteSerializer,
+14 -10
View File
@@ -24,7 +24,6 @@ from plane.db.models import (
)
from plane.utils.content_validator import (
validate_html_content,
validate_json_content,
validate_binary_data,
)
@@ -44,7 +43,8 @@ class IssueSerializer(BaseSerializer):
Comprehensive work item serializer with full relationship management.
Handles complete work item lifecycle including assignees, labels, validation,
and related model updates. Supports dynamic field expansion and HTML content processing.
and related model updates. Supports dynamic field expansion and HTML content
processing.
"""
assignees = serializers.ListField(
@@ -89,20 +89,24 @@ class IssueSerializer(BaseSerializer):
raise serializers.ValidationError("Invalid HTML passed")
# Validate description content for security
if data.get("description"):
is_valid, error_msg = validate_json_content(data["description"])
if not is_valid:
raise serializers.ValidationError({"description": error_msg})
if data.get("description_html"):
is_valid, error_msg = validate_html_content(data["description_html"])
is_valid, error_msg, sanitized_html = validate_html_content(
data["description_html"]
)
if not is_valid:
raise serializers.ValidationError({"description_html": error_msg})
raise serializers.ValidationError(
{"error": "html content is not valid"}
)
# Update the data with sanitized HTML if available
if sanitized_html is not None:
data["description_html"] = sanitized_html
if data.get("description_binary"):
is_valid, error_msg = validate_binary_data(data["description_binary"])
if not is_valid:
raise serializers.ValidationError({"description_binary": error_msg})
raise serializers.ValidationError(
{"description_binary": "Invalid binary data"}
)
# Validate assignees are from project
if data.get("assignees", []):
+6 -3
View File
@@ -17,7 +17,8 @@ class ModuleCreateSerializer(BaseSerializer):
"""
Serializer for creating modules with member validation and date checking.
Handles module creation including member assignment validation, date range verification,
Handles module creation including member assignment validation, date range
verification,
and duplicate name prevention for feature-based project organization setup.
"""
@@ -105,7 +106,8 @@ class ModuleUpdateSerializer(ModuleCreateSerializer):
"""
Serializer for updating modules with enhanced validation and member management.
Extends module creation with update-specific validations including member reassignment,
Extends module creation with update-specific validations including member
reassignment,
name conflict checking, and relationship management for module modifications.
"""
@@ -155,7 +157,8 @@ class ModuleSerializer(BaseSerializer):
"""
Comprehensive module serializer with work item metrics and member management.
Provides complete module data including work item counts by status, member relationships,
Provides complete module data including work item counts by status, member
relationships,
and progress tracking for feature-based project organization.
"""
+7 -17
View File
@@ -12,7 +12,6 @@ from plane.db.models import (
from plane.utils.content_validator import (
validate_html_content,
validate_json_content,
)
from .base import BaseSerializer
@@ -200,27 +199,18 @@ class ProjectSerializer(BaseSerializer):
)
# Validate description content for security
if "description" in data and data["description"]:
# For Project, description might be text field, not JSON
if isinstance(data["description"], dict):
is_valid, error_msg = validate_json_content(data["description"])
if not is_valid:
raise serializers.ValidationError({"description": error_msg})
if "description_text" in data and data["description_text"]:
is_valid, error_msg = validate_json_content(data["description_text"])
if not is_valid:
raise serializers.ValidationError({"description_text": error_msg})
if "description_html" in data and data["description_html"]:
if isinstance(data["description_html"], dict):
is_valid, error_msg = validate_json_content(data["description_html"])
else:
is_valid, error_msg = validate_html_content(
is_valid, error_msg, sanitized_html = validate_html_content(
str(data["description_html"])
)
# Update the data with sanitized HTML if available
if sanitized_html is not None:
data["description_html"] = sanitized_html
if not is_valid:
raise serializers.ValidationError({"description_html": error_msg})
raise serializers.ValidationError(
{"error": "html content is not valid"}
)
return data
+9 -5
View File
@@ -8,7 +8,7 @@ from django.conf import settings
# Third party imports
from rest_framework import status
from rest_framework.response import Response
from drf_spectacular.utils import OpenApiExample, OpenApiRequest, OpenApiTypes
from drf_spectacular.utils import OpenApiExample, OpenApiRequest
# Module Imports
from plane.bgtasks.storage_metadata_task import get_asset_object_metadata
@@ -106,7 +106,8 @@ class UserAssetEndpoint(BaseAPIView):
def post(self, request):
"""Generate presigned URL for user asset upload.
Create a presigned URL for uploading user profile assets (avatar or cover image).
Create a presigned URL for uploading user profile assets (avatar or cover
image).
This endpoint generates the necessary credentials for direct S3 upload.
"""
# get the asset key
@@ -201,8 +202,10 @@ class UserAssetEndpoint(BaseAPIView):
def patch(self, request, asset_id):
"""Update user asset after upload completion.
Update the asset status and attributes after the file has been uploaded to S3.
This endpoint should be called after completing the S3 upload to mark the asset as uploaded.
Update the asset status and attributes after the file has been uploaded to
S3.
This endpoint should be called after completing the S3 upload to mark the
asset as uploaded.
"""
# get the asset id
asset = FileAsset.objects.get(id=asset_id, user_id=request.user.id)
@@ -361,7 +364,8 @@ class UserServerAssetEndpoint(BaseAPIView):
"""Update user server asset after upload completion.
Update the asset status and attributes after the file has been uploaded to S3 using server credentials.
This endpoint should be called after completing the S3 upload to mark the asset as uploaded.
This endpoint should be called after completing the S3 upload to mark the
asset as uploaded.
"""
# get the asset id
asset = FileAsset.objects.get(id=asset_id, user_id=request.user.id)
-3
View File
@@ -30,12 +30,10 @@ from rest_framework.response import Response
# drf-spectacular imports
from drf_spectacular.utils import (
extend_schema,
OpenApiParameter,
OpenApiResponse,
OpenApiExample,
OpenApiRequest,
)
from drf_spectacular.types import OpenApiTypes
# Module imports
from plane.api.serializers import (
@@ -99,7 +97,6 @@ from plane.utils.openapi import (
EXTERNAL_ID_PARAMETER,
EXTERNAL_SOURCE_PARAMETER,
ORDER_BY_PARAMETER,
SEARCH_PARAMETER,
SEARCH_PARAMETER_REQUIRED,
LIMIT_PARAMETER,
WORKSPACE_SEARCH_PARAMETER,
+1 -3
View File
@@ -10,7 +10,7 @@ from django.core.serializers.json import DjangoJSONEncoder
# Third party imports
from rest_framework import status
from rest_framework.response import Response
from drf_spectacular.utils import OpenApiResponse, OpenApiExample, OpenApiRequest
from drf_spectacular.utils import OpenApiResponse, OpenApiRequest
# Module imports
from plane.api.serializers import (
@@ -41,8 +41,6 @@ from plane.utils.host import base_host
from plane.utils.openapi import (
module_docs,
module_issue_docs,
WORKSPACE_SLUG_PARAMETER,
PROJECT_ID_PARAMETER,
MODULE_ID_PARAMETER,
MODULE_PK_PARAMETER,
ISSUE_ID_PARAMETER,
+1 -1
View File
@@ -11,7 +11,7 @@ from django.core.serializers.json import DjangoJSONEncoder
from rest_framework import status
from rest_framework.response import Response
from rest_framework.serializers import ValidationError
from drf_spectacular.utils import OpenApiResponse, OpenApiExample, OpenApiRequest
from drf_spectacular.utils import OpenApiResponse, OpenApiRequest
# Module imports
+1 -1
View File
@@ -4,7 +4,7 @@ from django.db import IntegrityError
# Third party imports
from rest_framework import status
from rest_framework.response import Response
from drf_spectacular.utils import OpenApiResponse, OpenApiExample, OpenApiRequest
from drf_spectacular.utils import OpenApiResponse, OpenApiRequest
# Module imports
from plane.api.serializers import StateSerializer
+20 -2
View File
@@ -39,13 +39,31 @@ def allow_permission(allowed_roles, level="PROJECT", creator=False, model=None):
).exists():
return view_func(instance, request, *args, **kwargs)
else:
if ProjectMember.objects.filter(
is_user_has_allowed_role = ProjectMember.objects.filter(
member=request.user,
workspace__slug=kwargs["slug"],
project_id=kwargs["project_id"],
role__in=allowed_role_values,
is_active=True,
).exists():
).exists()
# Return if the user has the allowed role else if they are workspace admin and part of the project regardless of the role
if is_user_has_allowed_role:
return view_func(instance, request, *args, **kwargs)
elif (
ProjectMember.objects.filter(
member=request.user,
workspace__slug=kwargs["slug"],
project_id=kwargs["project_id"],
is_active=True,
).exists()
and WorkspaceMember.objects.filter(
member=request.user,
workspace__slug=kwargs["slug"],
role=ROLE.ADMIN.value,
is_active=True,
).exists()
):
return view_func(instance, request, *args, **kwargs)
# Return permission denied if no conditions are met
+22 -13
View File
@@ -3,11 +3,7 @@ from rest_framework.permissions import SAFE_METHODS, BasePermission
# Module import
from plane.db.models import ProjectMember, WorkspaceMember
# Permission Mappings
Admin = 20
Member = 15
Guest = 5
from plane.db.models.project import ROLE
class ProjectBasePermission(BasePermission):
@@ -26,18 +22,31 @@ class ProjectBasePermission(BasePermission):
return WorkspaceMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
role__in=[Admin, Member],
role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value],
is_active=True,
).exists()
## Only Project Admins can update project attributes
return ProjectMember.objects.filter(
project_member_qs = ProjectMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
role=Admin,
project_id=view.project_id,
is_active=True,
).exists()
)
## Only project admins or workspace admin who is part of the project can access
if project_member_qs.filter(role=ROLE.ADMIN.value).exists():
return True
else:
return (
project_member_qs.exists()
and WorkspaceMember.objects.filter(
member=request.user,
workspace__slug=view.workspace_slug,
role=ROLE.ADMIN.value,
is_active=True,
).exists()
)
class ProjectMemberPermission(BasePermission):
@@ -55,7 +64,7 @@ class ProjectMemberPermission(BasePermission):
return WorkspaceMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
role__in=[Admin, Member],
role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value],
is_active=True,
).exists()
@@ -63,7 +72,7 @@ class ProjectMemberPermission(BasePermission):
return ProjectMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
role__in=[Admin, Member],
role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value],
project_id=view.project_id,
is_active=True,
).exists()
@@ -97,7 +106,7 @@ class ProjectEntityPermission(BasePermission):
return ProjectMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
role__in=[Admin, Member],
role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value],
project_id=view.project_id,
is_active=True,
).exists()
+12 -10
View File
@@ -1,4 +1,3 @@
from lxml import html
# Django imports
from django.utils import timezone
@@ -23,7 +22,6 @@ from plane.db.models import (
)
from plane.utils.content_validator import (
validate_html_content,
validate_json_content,
validate_binary_data,
)
from plane.app.permissions import ROLE
@@ -76,20 +74,24 @@ class DraftIssueCreateSerializer(BaseSerializer):
raise serializers.ValidationError("Start date cannot exceed target date")
# Validate description content for security
if "description" in attrs and attrs["description"]:
is_valid, error_msg = validate_json_content(attrs["description"])
if not is_valid:
raise serializers.ValidationError({"description": error_msg})
if "description_html" in attrs and attrs["description_html"]:
is_valid, error_msg = validate_html_content(attrs["description_html"])
is_valid, error_msg, sanitized_html = validate_html_content(
attrs["description_html"]
)
if not is_valid:
raise serializers.ValidationError({"description_html": error_msg})
raise serializers.ValidationError(
{"error": "html content is not valid"}
)
# Update the attrs with sanitized HTML if available
if sanitized_html is not None:
attrs["description_html"] = sanitized_html
if "description_binary" in attrs and attrs["description_binary"]:
is_valid, error_msg = validate_binary_data(attrs["description_binary"])
if not is_valid:
raise serializers.ValidationError({"description_binary": error_msg})
raise serializers.ValidationError(
{"description_binary": "Invalid binary data"}
)
# Validate assignees are from project
if attrs.get("assignee_ids", []):
+32 -13
View File
@@ -1,4 +1,3 @@
from lxml import html
# Django imports
from django.utils import timezone
@@ -43,7 +42,6 @@ from plane.db.models import (
)
from plane.utils.content_validator import (
validate_html_content,
validate_json_content,
validate_binary_data,
)
@@ -128,20 +126,24 @@ class IssueCreateSerializer(BaseSerializer):
raise serializers.ValidationError("Start date cannot exceed target date")
# Validate description content for security
if "description" in attrs and attrs["description"]:
is_valid, error_msg = validate_json_content(attrs["description"])
if not is_valid:
raise serializers.ValidationError({"description": error_msg})
if "description_html" in attrs and attrs["description_html"]:
is_valid, error_msg = validate_html_content(attrs["description_html"])
is_valid, error_msg, sanitized_html = validate_html_content(
attrs["description_html"]
)
if not is_valid:
raise serializers.ValidationError({"description_html": error_msg})
raise serializers.ValidationError(
{"error": "html content is not valid"}
)
# Update the attrs with sanitized HTML if available
if sanitized_html is not None:
attrs["description_html"] = sanitized_html
if "description_binary" in attrs and attrs["description_binary"]:
is_valid, error_msg = validate_binary_data(attrs["description_binary"])
if not is_valid:
raise serializers.ValidationError({"description_binary": error_msg})
raise serializers.ValidationError(
{"description_binary": "Invalid binary data"}
)
# Validate assignees are from project
if attrs.get("assignee_ids", []):
@@ -664,16 +666,33 @@ class IssueReactionSerializer(BaseSerializer):
class IssueReactionLiteSerializer(DynamicBaseSerializer):
display_name = serializers.CharField(source="actor.display_name", read_only=True)
class Meta:
model = IssueReaction
fields = ["id", "actor", "issue", "reaction"]
fields = ["id", "actor", "issue", "reaction", "display_name"]
class CommentReactionSerializer(BaseSerializer):
display_name = serializers.CharField(source="actor.display_name", read_only=True)
class Meta:
model = CommentReaction
fields = "__all__"
read_only_fields = ["workspace", "project", "comment", "actor", "deleted_at"]
fields = [
"id",
"actor",
"comment",
"reaction",
"display_name",
"deleted_at",
"workspace",
"project",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
read_only_fields = ["workspace", "project", "comment", "actor", "deleted_at", "created_by", "updated_by"]
class IssueVoteSerializer(BaseSerializer):
+3 -14
View File
@@ -7,7 +7,6 @@ from .base import BaseSerializer
from plane.utils.content_validator import (
validate_binary_data,
validate_html_content,
validate_json_content,
)
from plane.db.models import (
Page,
@@ -229,23 +228,13 @@ class PageBinaryUpdateSerializer(serializers.Serializer):
return value
# Use the validation function from utils
is_valid, error_message = validate_html_content(value)
is_valid, error_message, sanitized_html = validate_html_content(value)
if not is_valid:
raise serializers.ValidationError(error_message)
return value
# Return sanitized HTML if available, otherwise return original
return sanitized_html if sanitized_html is not None else value
def validate_description(self, value):
"""Validate the JSON description"""
if not value:
return value
# Use the validation function from utils
is_valid, error_message = validate_json_content(value)
if not is_valid:
raise serializers.ValidationError(error_message)
return value
def update(self, instance, validated_data):
"""Update the page instance with validated data"""
+10 -21
View File
@@ -15,8 +15,6 @@ from plane.db.models import (
)
from plane.utils.content_validator import (
validate_html_content,
validate_json_content,
validate_binary_data,
)
@@ -65,27 +63,18 @@ class ProjectSerializer(BaseSerializer):
def validate(self, data):
# Validate description content for security
if "description" in data and data["description"]:
# For Project, description might be text field, not JSON
if isinstance(data["description"], dict):
is_valid, error_msg = validate_json_content(data["description"])
if not is_valid:
raise serializers.ValidationError({"description": error_msg})
if "description_text" in data and data["description_text"]:
is_valid, error_msg = validate_json_content(data["description_text"])
if not is_valid:
raise serializers.ValidationError({"description_text": error_msg})
if "description_html" in data and data["description_html"]:
if isinstance(data["description_html"], dict):
is_valid, error_msg = validate_json_content(data["description_html"])
else:
is_valid, error_msg = validate_html_content(
str(data["description_html"])
)
is_valid, error_msg, sanitized_html = validate_html_content(
str(data["description_html"])
)
# Update the data with sanitized HTML if available
if sanitized_html is not None:
data["description_html"] = sanitized_html
if not is_valid:
raise serializers.ValidationError({"description_html": error_msg})
raise serializers.ValidationError(
{"error": "html content is not valid"}
)
return data
+12 -9
View File
@@ -26,7 +26,6 @@ from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
from plane.utils.url import contains_url
from plane.utils.content_validator import (
validate_html_content,
validate_json_content,
validate_binary_data,
)
@@ -319,20 +318,24 @@ class StickySerializer(BaseSerializer):
def validate(self, data):
# Validate description content for security
if "description" in data and data["description"]:
is_valid, error_msg = validate_json_content(data["description"])
if not is_valid:
raise serializers.ValidationError({"description": error_msg})
if "description_html" in data and data["description_html"]:
is_valid, error_msg = validate_html_content(data["description_html"])
is_valid, error_msg, sanitized_html = validate_html_content(
data["description_html"]
)
if not is_valid:
raise serializers.ValidationError({"description_html": error_msg})
raise serializers.ValidationError(
{"error": "html content is not valid"}
)
# Update the data with sanitized HTML if available
if sanitized_html is not None:
data["description_html"] = sanitized_html
if "description_binary" in data and data["description_binary"]:
is_valid, error_msg = validate_binary_data(data["description_binary"])
if not is_valid:
raise serializers.ValidationError({"description_binary": error_msg})
raise serializers.ValidationError(
{"description_binary": "Invalid binary data"}
)
return data
+10 -2
View File
@@ -441,7 +441,11 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
# 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,
disposition="attachment",
filename=asset.attributes.get("name"),
)
# Redirect to the signed URL
return HttpResponseRedirect(signed_url)
@@ -641,7 +645,11 @@ class ProjectAssetEndpoint(BaseAPIView):
# 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,
disposition="attachment",
filename=asset.attributes.get("name"),
)
# Redirect to the signed URL
return HttpResponseRedirect(signed_url)
+8 -4
View File
@@ -369,11 +369,13 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
assignees__avatar_asset__isnull=False,
then=Concat(
Value("/api/assets/v2/static/"),
"assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field
"assignees__avatar_asset", # Assuming avatar_asset has an id or
# relevant field
Value("/"),
),
),
# If `avatar_asset` is None, fall back to using `avatar` field directly
# If `avatar_asset` is None, fall back to using `avatar` field
# directly
When(
assignees__avatar_asset__isnull=True,
then="assignees__avatar",
@@ -480,11 +482,13 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
assignees__avatar_asset__isnull=False,
then=Concat(
Value("/api/assets/v2/static/"),
"assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field
"assignees__avatar_asset", # Assuming avatar_asset has an id or
# relevant field
Value("/"),
),
),
# If `avatar_asset` is None, fall back to using `avatar` field directly
# If `avatar_asset` is None, fall back to using `avatar` field
# directly
When(
assignees__avatar_asset__isnull=True,
then="assignees__avatar",
+6 -3
View File
@@ -485,11 +485,13 @@ class ModuleViewSet(BaseViewSet):
assignees__avatar_asset__isnull=False,
then=Concat(
Value("/api/assets/v2/static/"),
"assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field
"assignees__avatar_asset", # Assuming avatar_asset has an id or
# relevant field
Value("/"),
),
),
# If `avatar_asset` is None, fall back to using `avatar` field directly
# If `avatar_asset` is None, fall back to using `avatar` field
# directly
When(
assignees__avatar_asset__isnull=True,
then="assignees__avatar",
@@ -597,7 +599,8 @@ class ModuleViewSet(BaseViewSet):
assignees__avatar_asset__isnull=False,
then=Concat(
Value("/api/assets/v2/static/"),
"assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field
"assignees__avatar_asset", # Assuming avatar_asset has an id or
# relevant field
Value("/"),
),
),
+2 -1
View File
@@ -112,7 +112,8 @@ class ModuleIssueViewSet(BaseViewSet):
if group_by == sub_group_by:
return Response(
{
"error": "Group by and sub group by cannot have same parameters"
"error": "Group by and sub group by cannot have same "
"parameters"
},
status=status.HTTP_400_BAD_REQUEST,
)
+13 -10
View File
@@ -1,6 +1,5 @@
# Python imports
import json
import base64
from datetime import datetime
from django.core.serializers.json import DjangoJSONEncoder
@@ -164,7 +163,8 @@ class PageViewSet(BaseViewSet):
):
return Response(
{
"error": "Access cannot be updated since this page is owned by someone else"
"error": "Access cannot be updated since this page is owned by "
"someone else"
},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -198,6 +198,7 @@ class PageViewSet(BaseViewSet):
def retrieve(self, request, slug, project_id, pk=None):
page = self.get_queryset().filter(pk=pk).first()
project = Project.objects.get(pk=project_id)
track_visit = request.query_params.get("track_visit", "true").lower() == "true"
"""
if the role is guest and guest_view_all_features is false and owned by is not
@@ -230,13 +231,14 @@ class PageViewSet(BaseViewSet):
).values_list("entity_identifier", flat=True)
data = PageDetailSerializer(page).data
data["issue_ids"] = issue_ids
recent_visited_task.delay(
slug=slug,
entity_name="page",
entity_identifier=pk,
user_id=request.user.id,
project_id=project_id,
)
if track_visit:
recent_visited_task.delay(
slug=slug,
entity_name="page",
entity_identifier=pk,
user_id=request.user.id,
project_id=project_id,
)
return Response(data, status=status.HTTP_200_OK)
@allow_permission([ROLE.ADMIN], model=Page, creator=True)
@@ -344,7 +346,8 @@ class PageViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
# if parent page is archived then the page will be un archived breaking the hierarchy
# if parent page is archived then the page will be un archived breaking
# the hierarchy
if page.parent_id and page.parent.archived_at:
page.parent = None
page.save(update_fields=["parent"])
+39 -13
View File
@@ -5,13 +5,12 @@ from django.utils import timezone
import json
# Django imports
from django.db import IntegrityError
from django.db.models import Exists, F, OuterRef, Prefetch, Q, Subquery
from django.core.serializers.json import DjangoJSONEncoder
# Third Party imports
from rest_framework.response import Response
from rest_framework import serializers, status
from rest_framework import status
from rest_framework.permissions import AllowAny
# Module imports
@@ -106,7 +105,10 @@ class ProjectViewSet(BaseViewSet):
fields = [field for field in request.GET.get("fields", "").split(",") if field]
projects = self.get_queryset().order_by("sort_order", "name")
if WorkspaceMember.objects.filter(
member=request.user, workspace__slug=slug, is_active=True, role=5
member=request.user,
workspace__slug=slug,
is_active=True,
role=ROLE.GUEST.value,
).exists():
projects = projects.filter(
project_projectmember__member=self.request.user,
@@ -114,7 +116,10 @@ class ProjectViewSet(BaseViewSet):
)
if WorkspaceMember.objects.filter(
member=request.user, workspace__slug=slug, is_active=True, role=15
member=request.user,
workspace__slug=slug,
is_active=True,
role=ROLE.MEMBER.value,
).exists():
projects = projects.filter(
Q(
@@ -189,7 +194,10 @@ class ProjectViewSet(BaseViewSet):
)
if WorkspaceMember.objects.filter(
member=request.user, workspace__slug=slug, is_active=True, role=5
member=request.user,
workspace__slug=slug,
is_active=True,
role=ROLE.GUEST.value,
).exists():
projects = projects.filter(
project_projectmember__member=self.request.user,
@@ -197,7 +205,10 @@ class ProjectViewSet(BaseViewSet):
)
if WorkspaceMember.objects.filter(
member=request.user, workspace__slug=slug, is_active=True, role=15
member=request.user,
workspace__slug=slug,
is_active=True,
role=ROLE.MEMBER.value,
).exists():
projects = projects.filter(
Q(
@@ -250,7 +261,9 @@ class ProjectViewSet(BaseViewSet):
# 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=ROLE.ADMIN.value,
)
# Also create the issue property for the user
_ = IssueUserProperty.objects.create(
@@ -263,7 +276,7 @@ class ProjectViewSet(BaseViewSet):
ProjectMember.objects.create(
project_id=serializer.data["id"],
member_id=serializer.data["project_lead"],
role=20,
role=ROLE.ADMIN.value,
)
# Also create the issue property for the user
IssueUserProperty.objects.create(
@@ -341,13 +354,23 @@ class ProjectViewSet(BaseViewSet):
def partial_update(self, request, slug, pk=None):
# try:
if not ProjectMember.objects.filter(
is_workspace_admin = WorkspaceMember.objects.filter(
member=request.user,
workspace__slug=slug,
is_active=True,
role=ROLE.ADMIN.value,
).exists()
is_project_admin = ProjectMember.objects.filter(
member=request.user,
workspace__slug=slug,
project_id=pk,
role=20,
role=ROLE.ADMIN.value,
is_active=True,
).exists():
).exists()
# Return error for if the user is neither workspace admin nor project admin
if not is_project_admin and not is_workspace_admin:
return Response(
{"error": "You don't have the required permissions."},
status=status.HTTP_403_FORBIDDEN,
@@ -402,13 +425,16 @@ class ProjectViewSet(BaseViewSet):
def destroy(self, request, slug, pk):
if (
WorkspaceMember.objects.filter(
member=request.user, workspace__slug=slug, is_active=True, role=20
member=request.user,
workspace__slug=slug,
is_active=True,
role=ROLE.ADMIN.value,
).exists()
or ProjectMember.objects.filter(
member=request.user,
workspace__slug=slug,
project_id=pk,
role=20,
role=ROLE.ADMIN.value,
is_active=True,
).exists()
):
+6 -3
View File
@@ -64,7 +64,8 @@ class ProjectInvitationsViewset(BaseViewSet):
if workspace_role in [5, 20] and workspace_role != email.get("role", 5):
return Response(
{
"error": "You cannot invite a user with different role than workspace role"
"error": "You cannot invite a user with different role than "
"workspace role"
}
)
@@ -91,7 +92,8 @@ class ProjectInvitationsViewset(BaseViewSet):
except ValidationError:
return Response(
{
"error": f"Invalid email - {email} provided a valid email address is required to send the invite"
"error": f"Invalid email - {email} provided a valid email address "
f"is required to send the invite"
},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -234,7 +236,8 @@ class ProjectJoinEndpoint(BaseAPIView):
workspace_member.is_active = True
workspace_member.save()
# Check if the user was already a member of project then activate the user
# Check if the user was already a member of project then activate
# the user
project_member = ProjectMember.objects.filter(
workspace_id=project_invite.workspace_id, member=user
).first()
+13 -6
View File
@@ -39,7 +39,8 @@ class ProjectMemberViewSet(BaseViewSet):
@allow_permission([ROLE.ADMIN])
def create(self, request, slug, project_id):
# Get the list of members to be added to the project and their roles i.e. the user_id and the role
# Get the list of members to be added to the project and their roles
# i.e. the user_id and the role
members = request.data.get("members", [])
# get the project
@@ -69,7 +70,8 @@ class ProjectMemberViewSet(BaseViewSet):
if workspace_member_role in [20] and member_roles.get(member) in [5, 15]:
return Response(
{
"error": "You cannot add a user with role lower than the workspace role"
"error": "You cannot add a user with role lower than the "
"workspace role"
},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -77,12 +79,14 @@ class ProjectMemberViewSet(BaseViewSet):
if workspace_member_role in [5] and member_roles.get(member) in [15, 20]:
return Response(
{
"error": "You cannot add a user with role higher than the workspace role"
"error": "You cannot add a user with role higher than the "
"workspace role"
},
status=status.HTTP_400_BAD_REQUEST,
)
# Update roles in the members array based on the member_roles dictionary and set is_active to True
# Update roles in the members array based on the member_roles dictionary
# and set is_active to True
for project_member in ProjectMember.objects.filter(
project_id=project_id,
member_id__in=[member.get("member_id") for member in members],
@@ -253,7 +257,8 @@ class ProjectMemberViewSet(BaseViewSet):
if str(project_member.id) == str(requesting_project_member.id):
return Response(
{
"error": "You cannot remove yourself from the workspace. Please use leave workspace"
"error": "You cannot remove yourself from the workspace. Please use "
"leave workspace"
},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -287,7 +292,9 @@ class ProjectMemberViewSet(BaseViewSet):
):
return Response(
{
"error": "You cannot leave the project as your the only admin of the project you will have to either delete the project or create an another admin"
"error": "You cannot leave the project as your the only admin of the "
"project you will have to either delete the project or create an "
"another admin"
},
status=status.HTTP_400_BAD_REQUEST,
)
+6 -3
View File
@@ -79,7 +79,8 @@ class UserEndpoint(BaseViewSet):
if InstanceAdmin.objects.filter(user=user).exists():
return Response(
{
"error": "You cannot deactivate your account since you are an instance admin"
"error": "You cannot deactivate your account since you are an "
"instance admin"
},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -107,7 +108,8 @@ class UserEndpoint(BaseViewSet):
else:
return Response(
{
"error": "You cannot deactivate account as you are the only admin in some projects."
"error": "You cannot deactivate account as you are the only admin "
"in some projects."
},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -132,7 +134,8 @@ class UserEndpoint(BaseViewSet):
else:
return Response(
{
"error": "You cannot deactivate account as you are the only admin in some workspaces."
"error": "You cannot deactivate account as you are the only admin "
"in some workspaces."
},
status=status.HTTP_400_BAD_REQUEST,
)
+2 -1
View File
@@ -147,7 +147,8 @@ class WorkspaceViewViewSet(BaseViewSet):
class WorkspaceViewIssuesViewSet(BaseViewSet):
def _get_project_permission_filters(self):
"""
Get common project permission filters for guest users and role-based access control.
Get common project permission filters for guest users and role-based
access control.
Returns Q object for filtering issues based on user role and project settings.
"""
return Q(
+2 -4
View File
@@ -39,9 +39,6 @@ from plane.db.models import (
Profile,
)
from plane.app.permissions import ROLE, allow_permission
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_control
from django.views.decorators.vary import vary_on_cookie
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
from plane.license.utils.instance_value import get_configuration_value
from plane.bgtasks.workspace_seed_task import workspace_seed
@@ -393,7 +390,8 @@ class ExportWorkspaceUserActivityEndpoint(BaseAPIView):
rows = [
(
activity.actor.display_name,
f"{activity.project.identifier} - {activity.issue.sequence_id if activity.issue else ''}",
f"{activity.project.identifier} - "
f"{activity.issue.sequence_id if activity.issue else ''}",
activity.project.name,
activity.created_at,
activity.updated_at,
+3 -1
View File
@@ -172,12 +172,14 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
{"error": "Issue not found"}, status=status.HTTP_404_NOT_FOUND
)
project_id = request.data.get("project_id", issue.project_id)
serializer = DraftIssueCreateSerializer(
issue,
data=request.data,
partial=True,
context={
"project_id": request.data.get("project_id", None),
"project_id": project_id,
"cycle_id": request.data.get("cycle_id", "not_provided"),
},
)
+6 -3
View File
@@ -113,7 +113,8 @@ class WorkspaceInvitationsViewset(BaseViewSet):
except ValidationError:
return Response(
{
"error": f"Invalid email - {email} provided a valid email address is required to send the invite"
"error": f"Invalid email - {email} provided a valid email address "
f"is required to send the invite"
},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -185,7 +186,8 @@ class WorkspaceJoinEndpoint(BaseAPIView):
# If the user is present then create the workspace member
if user is not None:
# Check if the user was already a member of workspace then activate the user
# Check if the user was already a member of workspace then
# activate the user
workspace_member = WorkspaceMember.objects.filter(
workspace=workspace_invite.workspace, member=user
).first()
@@ -262,7 +264,8 @@ class UserWorkspaceInvitationsViewSet(BaseViewSet):
pk__in=invitations, email=request.user.email
).order_by("-created_at")
# If the user is already a member of workspace and was deactivated then activate the user
# If the user is already a member of workspace and was deactivated then
# activate the user
for invitation in workspace_invitations:
invalidate_cache_directly(
path=f"/api/workspaces/{invitation.workspace.slug}/members/",
+11 -4
View File
@@ -99,7 +99,8 @@ class WorkSpaceMemberViewSet(BaseViewSet):
if str(workspace_member.id) == str(requesting_workspace_member.id):
return Response(
{
"error": "You cannot remove yourself from the workspace. Please use leave workspace"
"error": "You cannot remove yourself from the workspace. Please use "
"leave workspace"
},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -126,7 +127,9 @@ class WorkSpaceMemberViewSet(BaseViewSet):
):
return Response(
{
"error": "User is a part of some projects where they are the only admin, they should either leave that project or promote another user to admin."
"error": "User is a part of some projects where they are the only "
"admin, they should either leave that project or promote another "
"user to admin."
},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -166,7 +169,9 @@ class WorkSpaceMemberViewSet(BaseViewSet):
):
return Response(
{
"error": "You cannot leave the workspace as you are the only admin of the workspace you will have to either delete the workspace or promote another user to admin."
"error": "You cannot leave the workspace as you are the only admin of "
"the workspace you will have to either delete the workspace or "
"promote another user to admin."
},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -187,7 +192,9 @@ class WorkSpaceMemberViewSet(BaseViewSet):
):
return Response(
{
"error": "You are a part of some projects where you are the only admin, you should either leave the project or promote another user to admin."
"error": "You are a part of some projects where you are the only "
"admin, you should either leave the project or promote another "
"user to admin."
},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -7,7 +7,6 @@ from plane.app.serializers import StateSerializer
from plane.app.views.base import BaseAPIView
from plane.db.models import State
from plane.app.permissions import WorkspaceEntityPermission
from plane.utils.cache import cache_response
from collections import defaultdict
@@ -15,7 +14,6 @@ class WorkspaceStatesEndpoint(BaseAPIView):
permission_classes = [WorkspaceEntityPermission]
use_read_replica = True
@cache_response(60 * 60 * 2)
def get(self, request, slug):
states = State.objects.filter(
workspace__slug=slug,
+2 -1
View File
@@ -157,7 +157,8 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
if group_by == sub_group_by:
return Response(
{
"error": "Group by and sub group by cannot have same parameters"
"error": "Group by and sub group by cannot have same "
"parameters"
},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -8,7 +8,8 @@ from plane.utils.cache import invalidate_cache_directly
def process_workspace_project_invitations(user):
"""This function takes in User and adds him to all workspace and projects that the user has accepted invited of"""
"""This function takes in User and adds him to all workspace and projects
that the user has accepted invited of"""
# Check if user has any accepted invites for workspace and add them to workspace
workspace_member_invites = WorkspaceMemberInvite.objects.filter(
@@ -107,7 +107,8 @@ class MagicSignInEndpoint(View):
# Login the user and record his device info
user_login(request=request, user=user, is_app=True)
if user.is_password_autoset and profile.is_onboarded:
path = "accounts/set-password"
# Redirect to the home page
path = "/"
else:
# Get the redirection path
path = (
@@ -108,7 +108,8 @@ class SetUserPasswordEndpoint(APIView):
error_code=AUTHENTICATION_ERROR_CODES["PASSWORD_ALREADY_SET"],
error_message="PASSWORD_ALREADY_SET",
payload={
"error": "Your password is already set please change your password from profile"
"error": "Your password is already set please change your password "
"from profile"
},
)
return Response(exc.get_error_dict(), status=status.HTTP_400_BAD_REQUEST)
@@ -95,7 +95,8 @@ class SignInAuthSpaceEndpoint(View):
# Login the user and record his device info
user_login(request=request, user=user, is_space=True)
# redirect to next path
url = f"{base_host(request=request, is_space=True)}{str(next_path) if next_path else ''}"
base_url = base_host(request=request, is_space=True)
url = f"{base_url}{str(next_path) if next_path else ''}"
return HttpResponseRedirect(url)
except AuthenticationException as e:
params = e.get_error_dict()
@@ -179,7 +180,8 @@ class SignUpAuthSpaceEndpoint(View):
# Login the user and record his device info
user_login(request=request, user=user, is_space=True)
# redirect to referer path
url = f"{base_host(request=request, is_space=True)}{str(next_path) if next_path else ''}"
base_url = base_host(request=request, is_space=True)
url = f"{base_url}{str(next_path) if next_path else ''}"
return HttpResponseRedirect(url)
except AuthenticationException as e:
params = e.get_error_dict()
@@ -89,7 +89,8 @@ class GitHubCallbackSpaceEndpoint(View):
user_login(request=request, user=user, is_space=True)
# Process workspace and project invitations
# redirect to referer path
url = f"{base_host(request=request, is_space=True)}{str(next_path) if next_path else ''}"
base_url = base_host(request=request, is_space=True)
url = f"{base_url}{str(next_path) if next_path else ''}"
return HttpResponseRedirect(url)
except AuthenticationException as e:
params = e.get_error_dict()
@@ -89,7 +89,8 @@ class GitLabCallbackSpaceEndpoint(View):
user_login(request=request, user=user, is_space=True)
# Process workspace and project invitations
# redirect to referer path
url = f"{base_host(request=request, is_space=True)}{str(next_path) if next_path else ''}"
base_url = base_host(request=request, is_space=True)
url = f"{base_url}{str(next_path) if next_path else ''}"
return HttpResponseRedirect(url)
except AuthenticationException as e:
params = e.get_error_dict()
@@ -85,7 +85,8 @@ class GoogleCallbackSpaceEndpoint(View):
# Login the user and record his device info
user_login(request=request, user=user, is_space=True)
# redirect to referer path
url = f"{base_host(request=request, is_space=True)}{str(next_path) if next_path else ''}"
base_url = base_host(request=request, is_space=True)
url = f"{base_url}{str(next_path) if next_path else ''}"
return HttpResponseRedirect(url)
except AuthenticationException as e:
params = e.get_error_dict()
@@ -146,7 +146,8 @@ class MagicSignUpSpaceEndpoint(View):
# Login the user and record his device info
user_login(request=request, user=user, is_space=True)
# redirect to referer path
url = f"{base_host(request=request, is_space=True)}{str(next_path) if next_path else ''}"
base_url = base_host(request=request, is_space=True)
url = f"{base_url}{str(next_path) if next_path else ''}"
return HttpResponseRedirect(url)
except AuthenticationException as e:
@@ -120,7 +120,8 @@ class ResetPasswordSpaceEndpoint(View):
error_message="INVALID_PASSWORD_TOKEN",
)
params = exc.get_error_dict()
url = f"{base_host(request=request, is_space=True)}/accounts/reset-password/?{urlencode(params)}"
base_url = base_host(request=request, is_space=True)
url = f"{base_url}/accounts/reset-password/?{urlencode(params)}"
return HttpResponseRedirect(url)
password = request.POST.get("password", False)
@@ -130,7 +131,8 @@ class ResetPasswordSpaceEndpoint(View):
error_code=AUTHENTICATION_ERROR_CODES["INVALID_PASSWORD"],
error_message="INVALID_PASSWORD",
)
url = f"{base_host(request=request, is_space=True)}/accounts/reset-password/?{urlencode(exc.get_error_dict())}"
base_url = base_host(request=request, is_space=True)
url = f"{base_url}/accounts/reset-password/?{urlencode(exc.get_error_dict())}"
return HttpResponseRedirect(url)
# Check the password complexity
@@ -140,7 +142,8 @@ class ResetPasswordSpaceEndpoint(View):
error_code=AUTHENTICATION_ERROR_CODES["INVALID_PASSWORD"],
error_message="INVALID_PASSWORD",
)
url = f"{base_host(request=request, is_space=True)}/accounts/reset-password/?{urlencode(exc.get_error_dict())}"
base_url = base_host(request=request, is_space=True)
url = f"{base_url}/accounts/reset-password/?{urlencode(exc.get_error_dict())}"
return HttpResponseRedirect(url)
# set_password also hashes the password that the user will get
@@ -22,8 +22,10 @@ class SignOutAuthSpaceEndpoint(View):
user.save()
# Log the user out
logout(request)
url = f"{base_host(request=request, is_space=True)}{str(validate_next_path(next_path)) if next_path else ''}"
base_url = base_host(request=request, is_space=True)
url = f"{base_url}{str(validate_next_path(next_path)) if next_path else ''}"
return HttpResponseRedirect(url)
except Exception:
url = f"{base_host(request=request, is_space=True)}{str(validate_next_path(next_path)) if next_path else ''}"
base_url = base_host(request=request, is_space=True)
url = f"{base_url}{str(validate_next_path(next_path)) if next_path else ''}"
return HttpResponseRedirect(url)
+24 -24
View File
@@ -67,7 +67,7 @@ def flush_to_mongo_and_delete(
mongo_archival_failed = False
# Try to insert into MongoDB if available
if mongo_collection and mongo_available:
if mongo_collection is not None and mongo_available:
try:
mongo_collection.bulk_write([InsertOne(doc) for doc in buffer])
except BulkWriteError as bwe:
@@ -166,9 +166,9 @@ def process_cleanup_task(
def transform_api_log(record: Dict) -> Dict:
"""Transform API activity log record."""
return {
"id": record["id"],
"id": str(record["id"]),
"created_at": str(record["created_at"]) if record.get("created_at") else None,
"token_identifier": record["token_identifier"],
"token_identifier": str(record["token_identifier"]),
"path": record["path"],
"method": record["method"],
"query_params": record.get("query_params"),
@@ -178,18 +178,18 @@ def transform_api_log(record: Dict) -> Dict:
"response_body": record["response_body"],
"ip_address": record["ip_address"],
"user_agent": record["user_agent"],
"created_by_id": record["created_by_id"],
"created_by_id": str(record["created_by_id"]),
}
def transform_email_log(record: Dict) -> Dict:
"""Transform email notification log record."""
return {
"id": record["id"],
"id": str(record["id"]),
"created_at": str(record["created_at"]) if record.get("created_at") else None,
"receiver_id": record["receiver_id"],
"triggered_by_id": record["triggered_by_id"],
"entity_identifier": record["entity_identifier"],
"receiver_id": str(record["receiver_id"]),
"triggered_by_id": str(record["triggered_by_id"]),
"entity_identifier": str(record["entity_identifier"]),
"entity_name": record["entity_name"],
"data": record["data"],
"processed_at": (
@@ -197,27 +197,27 @@ def transform_email_log(record: Dict) -> Dict:
),
"sent_at": str(record["sent_at"]) if record.get("sent_at") else None,
"entity": record["entity"],
"old_value": record["old_value"],
"new_value": record["new_value"],
"created_by_id": record["created_by_id"],
"old_value": str(record["old_value"]),
"new_value": str(record["new_value"]),
"created_by_id": str(record["created_by_id"]),
}
def transform_page_version(record: Dict) -> Dict:
"""Transform page version record."""
return {
"id": record["id"],
"id": str(record["id"]),
"created_at": str(record["created_at"]) if record.get("created_at") else None,
"page_id": record["page_id"],
"workspace_id": record["workspace_id"],
"owned_by_id": record["owned_by_id"],
"page_id": str(record["page_id"]),
"workspace_id": str(record["workspace_id"]),
"owned_by_id": str(record["owned_by_id"]),
"description_html": record["description_html"],
"description_binary": record["description_binary"],
"description_stripped": record["description_stripped"],
"description_json": record["description_json"],
"sub_pages_data": record["sub_pages_data"],
"created_by_id": record["created_by_id"],
"updated_by_id": record["updated_by_id"],
"created_by_id": str(record["created_by_id"]),
"updated_by_id": str(record["updated_by_id"]),
"deleted_at": str(record["deleted_at"]) if record.get("deleted_at") else None,
"last_saved_at": (
str(record["last_saved_at"]) if record.get("last_saved_at") else None
@@ -228,14 +228,14 @@ def transform_page_version(record: Dict) -> Dict:
def transform_issue_description_version(record: Dict) -> Dict:
"""Transform issue description version record."""
return {
"id": record["id"],
"id": str(record["id"]),
"created_at": str(record["created_at"]) if record.get("created_at") else None,
"issue_id": record["issue_id"],
"workspace_id": record["workspace_id"],
"project_id": record["project_id"],
"created_by_id": record["created_by_id"],
"updated_by_id": record["updated_by_id"],
"owned_by_id": record["owned_by_id"],
"issue_id": str(record["issue_id"]),
"workspace_id": str(record["workspace_id"]),
"project_id": str(record["project_id"]),
"created_by_id": str(record["created_by_id"]),
"updated_by_id": str(record["updated_by_id"]),
"owned_by_id": str(record["owned_by_id"]),
"last_saved_at": (
str(record["last_saved_at"]) if record.get("last_saved_at") else None
),
@@ -30,6 +30,8 @@ def page_version(page_id, existing_instance, user_id):
description_binary=page.description_binary,
owned_by_id=user_id,
last_saved_at=page.updated_at,
description_json=page.description,
description_stripped=page.description_stripped,
)
# If page versions are greater than 20 delete the oldest one
+3 -2
View File
@@ -436,7 +436,7 @@ def webhook_activity(
def model_activity(
model_name, model_id, requested_data, current_instance, actor_id, slug, origin=None
):
"""Function takes in two json and computes differences between keys of both the json"""
"""Function takes in two json and computes differences between keys of both the json."""
if current_instance is None:
webhook_activity.delay(
event=model_name,
@@ -458,7 +458,8 @@ def model_activity(
json.loads(current_instance) if current_instance is not None else None
)
# Loop through all keys in requested data and check the current value and requested value
# Loop through all keys in requested data and check the current value and
# requested value
for key in requested_data:
# Check if key is present in current instance or not
if key in current_instance:
+10 -3
View File
@@ -76,7 +76,8 @@ def create_project_and_member(workspace: Workspace) -> Dict[int, uuid.UUID]:
if not project_seeds:
logger.warning(
"Task: workspace_seed_task -> No project seeds found. Skipping project creation."
"Task: workspace_seed_task -> No project seeds found. "
"Skipping project creation."
)
return projects_map
@@ -92,6 +93,10 @@ def create_project_and_member(workspace: Workspace) -> Dict[int, uuid.UUID]:
name=workspace.name, # Use workspace name
identifier=project_identifier,
created_by_id=workspace.created_by_id,
# Enable all views in seed data
cycle_view=True,
module_view=True,
issue_views_view=True,
)
# Create project members
@@ -231,7 +236,8 @@ def create_project_issues(
for field in required_fields:
if field not in issue_seed:
logger.error(
f"Task: workspace_seed_task -> Required field '{field}' missing in issue seed"
f"Task: workspace_seed_task -> Required field '{field}' "
f"missing in issue seed"
)
continue
@@ -314,6 +320,7 @@ def workspace_seed(workspace_id: uuid.UUID) -> None:
return
except Exception as e:
logger.error(
f"Task: workspace_seed_task -> Failed to seed workspace {workspace_id}: {str(e)}"
f"Task: workspace_seed_task -> Failed to seed workspace "
f"{workspace_id}: {str(e)}"
)
raise e
@@ -62,7 +62,8 @@ class Command(BaseCommand):
# Access to the bucket is forbidden
self.stdout.write(
self.style.ERROR(
f"Access to the bucket '{bucket_name}' is forbidden. Check permissions."
f"Access to the bucket '{bucket_name}' is forbidden. "
f"Check permissions."
)
)
else:
@@ -61,7 +61,8 @@ class Command(BaseCommand):
)
)
with transaction.atomic():
# This ensures only one transaction per project can execute this code at a time
# This ensures only one transaction per project can execute this
# code at a time
lock_key = convert_uuid_to_integer(project.id)
# Acquire an exclusive lock using the project ID as the lock key
@@ -37,7 +37,8 @@ class Command(BaseCommand):
if workspace.deleted_at is None:
self.stdout.write(
self.style.WARNING(
f"Workspace '{workspace.name}' (slug: {workspace.slug}) is not deleted."
f"Workspace '{workspace.name}' (slug: {workspace.slug}) "
f"is not deleted."
)
)
return
@@ -46,7 +47,8 @@ class Command(BaseCommand):
if "__" in workspace.slug and workspace.slug.split("__")[-1].isdigit():
self.stdout.write(
self.style.WARNING(
f"Workspace '{workspace.name}' (slug: {workspace.slug}) already has a timestamp appended."
f"Workspace '{workspace.name}' (slug: {workspace.slug}) "
f"already has a timestamp appended."
)
)
return
@@ -59,7 +61,8 @@ class Command(BaseCommand):
if dry_run:
self.stdout.write(
f"Would update workspace '{workspace.name}' slug from '{workspace.slug}' to '{new_slug}'"
f"Would update workspace '{workspace.name}' slug from "
f"'{workspace.slug}' to '{new_slug}'"
)
else:
try:
@@ -68,7 +71,8 @@ class Command(BaseCommand):
workspace.save(update_fields=["slug"])
self.stdout.write(
self.style.SUCCESS(
f"Updated workspace '{workspace.name}' slug from '{workspace.slug}' to '{new_slug}'"
f"Updated workspace '{workspace.name}' slug from "
f"'{workspace.slug}' to '{new_slug}'"
)
)
except Exception as e:
@@ -0,0 +1,30 @@
# Generated by Django 4.2.22 on 2025-08-29 11:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("db", "0101_description_descriptionversion"),
]
operations = [
migrations.AddField(
model_name="page",
name="sort_order",
field=models.FloatField(default=65535),
),
migrations.AddField(
model_name="pagelog",
name="entity_type",
field=models.CharField(
blank=True, max_length=30, null=True, verbose_name="Entity Type"
),
),
migrations.AlterField(
model_name="pagelog",
name="entity_identifier",
field=models.UUIDField(blank=True, null=True),
),
]
@@ -0,0 +1,75 @@
# Generated by Django 4.2.22 on 2025-09-01 14:33
from django.db import migrations, models
from django.contrib.postgres.operations import AddIndexConcurrently
class Migration(migrations.Migration):
atomic = False
dependencies = [
('db', '0102_page_sort_order_pagelog_entity_type_and_more'),
]
operations = [
AddIndexConcurrently(
model_name='fileasset',
index=models.Index(fields=['entity_type'], name='asset_entity_type_idx'),
),
AddIndexConcurrently(
model_name='fileasset',
index=models.Index(fields=['entity_identifier'], name='asset_entity_identifier_idx'),
),
AddIndexConcurrently(
model_name='fileasset',
index=models.Index(fields=['entity_type', 'entity_identifier'], name='asset_entity_idx'),
),
AddIndexConcurrently(
model_name='notification',
index=models.Index(fields=['entity_identifier'], name='notif_entity_identifier_idx'),
),
AddIndexConcurrently(
model_name='notification',
index=models.Index(fields=['entity_name'], name='notif_entity_name_idx'),
),
AddIndexConcurrently(
model_name='notification',
index=models.Index(fields=['read_at'], name='notif_read_at_idx'),
),
AddIndexConcurrently(
model_name='notification',
index=models.Index(fields=['receiver', 'read_at'], name='notif_entity_idx'),
),
AddIndexConcurrently(
model_name='pagelog',
index=models.Index(fields=['entity_type'], name='pagelog_entity_type_idx'),
),
AddIndexConcurrently(
model_name='pagelog',
index=models.Index(fields=['entity_identifier'], name='pagelog_entity_id_idx'),
),
AddIndexConcurrently(
model_name='pagelog',
index=models.Index(fields=['entity_name'], name='pagelog_entity_name_idx'),
),
AddIndexConcurrently(
model_name='pagelog',
index=models.Index(fields=['entity_type', 'entity_identifier'], name='pagelog_type_id_idx'),
),
AddIndexConcurrently(
model_name='pagelog',
index=models.Index(fields=['entity_name', 'entity_identifier'], name='pagelog_name_id_idx'),
),
AddIndexConcurrently(
model_name='userfavorite',
index=models.Index(fields=['entity_type'], name='fav_entity_type_idx'),
),
AddIndexConcurrently(
model_name='userfavorite',
index=models.Index(fields=['entity_identifier'], name='fav_entity_identifier_idx'),
),
AddIndexConcurrently(
model_name='userfavorite',
index=models.Index(fields=['entity_type', 'entity_identifier'], name='fav_entity_idx'),
),
]
@@ -0,0 +1,43 @@
# Generated by Django 4.2.22 on 2025-09-03 05:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('db', '0103_fileasset_asset_entity_type_idx_and_more'),
]
operations = [
migrations.AddField(
model_name='cycleuserproperties',
name='rich_filters',
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name='exporterhistory',
name='rich_filters',
field=models.JSONField(blank=True, default=dict, null=True),
),
migrations.AddField(
model_name='issueuserproperty',
name='rich_filters',
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name='issueview',
name='rich_filters',
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name='moduleuserproperties',
name='rich_filters',
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name='workspaceuserproperties',
name='rich_filters',
field=models.JSONField(default=dict),
),
]
@@ -0,0 +1,33 @@
# Generated by Django 4.2.22 on 2025-09-10 09:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("db", "0104_cycleuserproperties_rich_filters_and_more"),
]
operations = [
migrations.AlterField(
model_name="project",
name="cycle_view",
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name="project",
name="issue_views_view",
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name="project",
name="module_view",
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name="session",
name="user_id",
field=models.CharField(db_index=True, max_length=50, null=True),
),
]
+17 -2
View File
@@ -76,6 +76,15 @@ class FileAsset(BaseModel):
verbose_name_plural = "File Assets"
db_table = "file_assets"
ordering = ("-created_at",)
indexes = [
models.Index(fields=["entity_type"], name="asset_entity_type_idx"),
models.Index(
fields=["entity_identifier"], name="asset_entity_identifier_idx"
),
models.Index(
fields=["entity_type", "entity_identifier"], name="asset_entity_idx"
),
]
def __str__(self):
return str(self.asset)
@@ -91,7 +100,10 @@ class FileAsset(BaseModel):
return f"/api/assets/v2/static/{self.id}/"
if self.entity_type == self.EntityTypeContext.ISSUE_ATTACHMENT:
return f"/api/assets/v2/workspaces/{self.workspace.slug}/projects/{self.project_id}/issues/{self.issue_id}/attachments/{self.id}/"
return (
f"/api/assets/v2/workspaces/{self.workspace.slug}/projects/"
f"{self.project_id}/issues/{self.issue_id}/attachments/{self.id}/"
)
if self.entity_type in [
self.EntityTypeContext.ISSUE_DESCRIPTION,
@@ -99,6 +111,9 @@ class FileAsset(BaseModel):
self.EntityTypeContext.PAGE_DESCRIPTION,
self.EntityTypeContext.DRAFT_ISSUE_DESCRIPTION,
]:
return f"/api/assets/v2/workspaces/{self.workspace.slug}/projects/{self.project_id}/{self.id}/"
return (
f"/api/assets/v2/workspaces/{self.workspace.slug}/projects/"
f"{self.project_id}/{self.id}/"
)
return None
+1
View File
@@ -139,6 +139,7 @@ class CycleUserProperties(ProjectBaseModel):
filters = models.JSONField(default=get_default_filters)
display_filters = models.JSONField(default=get_default_display_filters)
display_properties = models.JSONField(default=get_default_display_properties)
rich_filters = models.JSONField(default=dict)
class Meta:
unique_together = ["cycle", "user", "deleted_at"]
+1
View File
@@ -56,6 +56,7 @@ class ExporterHistory(BaseModel):
related_name="workspace_exporters",
)
filters = models.JSONField(blank=True, null=True)
rich_filters = models.JSONField(default=dict, blank=True, null=True)
class Meta:
verbose_name = "Exporter"
+9
View File
@@ -41,6 +41,15 @@ class UserFavorite(WorkspaceBaseModel):
verbose_name_plural = "User Favorites"
db_table = "user_favorites"
ordering = ("-created_at",)
indexes = [
models.Index(fields=["entity_type"], name="fav_entity_type_idx"),
models.Index(
fields=["entity_identifier"], name="fav_entity_identifier_idx"
),
models.Index(
fields=["entity_type", "entity_identifier"], name="fav_entity_idx"
),
]
def save(self, *args, **kwargs):
if self._state.adding:
+5 -2
View File
@@ -210,7 +210,8 @@ class Issue(ProjectBaseModel):
if self._state.adding:
with transaction.atomic():
# Create a lock for this specific project using an advisory lock
# This ensures only one transaction per project can execute this code at a time
# This ensures only one transaction per project can execute this
# code at a time
lock_key = convert_uuid_to_integer(self.project.id)
with connection.cursor() as cursor:
@@ -509,6 +510,7 @@ class IssueUserProperty(ProjectBaseModel):
filters = models.JSONField(default=get_default_filters)
display_filters = models.JSONField(default=get_default_display_filters)
display_properties = models.JSONField(default=get_default_display_properties)
rich_filters = models.JSONField(default=dict)
class Meta:
verbose_name = "Issue User Property"
@@ -552,7 +554,8 @@ class IssueSequence(ProjectBaseModel):
Issue,
on_delete=models.SET_NULL,
related_name="issue_sequence",
null=True, # This is set to null because we want to keep the sequence even if the issue is deleted
null=True, # This is set to null because we want to keep the sequence
# even if the issue is deleted
)
sequence = models.PositiveBigIntegerField(default=1, db_index=True)
deleted = models.BooleanField(default=False)
+2 -1
View File
@@ -27,7 +27,8 @@ class Label(WorkspaceBaseModel):
condition=Q(project__isnull=True, deleted_at__isnull=True),
name="unique_name_when_project_null_and_not_deleted",
),
# Enforce uniqueness of project and name when project is not NULL and deleted_at is NULL
# Enforce uniqueness of project and name when project is not NULL
# and deleted_at is NULL
models.UniqueConstraint(
fields=["project", "name"],
condition=Q(project__isnull=False, deleted_at__isnull=True),
+1
View File
@@ -207,6 +207,7 @@ class ModuleUserProperties(ProjectBaseModel):
filters = models.JSONField(default=get_default_filters)
display_filters = models.JSONField(default=get_default_display_filters)
display_properties = models.JSONField(default=get_default_display_properties)
rich_filters = models.JSONField(default=dict)
class Meta:
unique_together = ["module", "user", "deleted_at"]
+8
View File
@@ -39,6 +39,14 @@ class Notification(BaseModel):
verbose_name_plural = "Notifications"
db_table = "notifications"
ordering = ("-created_at",)
indexes = [
models.Index(
fields=["entity_identifier"], name="notif_entity_identifier_idx"
),
models.Index(fields=["entity_name"], name="notif_entity_name_idx"),
models.Index(fields=["read_at"], name="notif_read_at_idx"),
models.Index(fields=["receiver", "read_at"], name="notif_entity_idx"),
]
def __str__(self):
"""Return name of the notifications"""
+16 -1
View File
@@ -57,6 +57,7 @@ class Page(BaseModel):
)
moved_to_page = models.UUIDField(null=True, blank=True)
moved_to_project = models.UUIDField(null=True, blank=True)
sort_order = models.FloatField(default=65535)
external_id = models.CharField(max_length=255, null=True, blank=True)
external_source = models.CharField(max_length=255, null=True, blank=True)
@@ -98,8 +99,11 @@ class PageLog(BaseModel):
)
transaction = models.UUIDField(default=uuid.uuid4)
page = models.ForeignKey(Page, related_name="page_log", on_delete=models.CASCADE)
entity_identifier = models.UUIDField(null=True)
entity_identifier = models.UUIDField(null=True, blank=True)
entity_name = models.CharField(max_length=30, verbose_name="Transaction Type")
entity_type = models.CharField(
max_length=30, verbose_name="Entity Type", null=True, blank=True
)
workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="workspace_page_log"
)
@@ -110,6 +114,17 @@ class PageLog(BaseModel):
verbose_name_plural = "Page Logs"
db_table = "page_logs"
ordering = ("-created_at",)
indexes = [
models.Index(fields=["entity_type"], name="pagelog_entity_type_idx"),
models.Index(fields=["entity_identifier"], name="pagelog_entity_id_idx"),
models.Index(fields=["entity_name"], name="pagelog_entity_name_idx"),
models.Index(
fields=["entity_type", "entity_identifier"], name="pagelog_type_id_idx"
),
models.Index(
fields=["entity_name", "entity_identifier"], name="pagelog_name_id_idx"
),
]
def __str__(self):
return f"{self.page.name} {self.entity_name}"
+9 -3
View File
@@ -18,6 +18,12 @@ from .base import BaseModel
ROLE_CHOICES = ((20, "Admin"), (15, "Member"), (5, "Guest"))
class ROLE(Enum):
ADMIN = 20
MEMBER = 15
GUEST = 5
class ProjectNetwork(Enum):
SECRET = 0
PUBLIC = 2
@@ -89,9 +95,9 @@ class Project(BaseModel):
)
emoji = models.CharField(max_length=255, null=True, blank=True)
icon_prop = models.JSONField(null=True)
module_view = models.BooleanField(default=True)
cycle_view = models.BooleanField(default=True)
issue_views_view = models.BooleanField(default=True)
module_view = models.BooleanField(default=False)
cycle_view = models.BooleanField(default=False)
issue_views_view = models.BooleanField(default=False)
page_view = models.BooleanField(default=True)
intake_view = models.BooleanField(default=False)
is_time_tracking_enabled = models.BooleanField(default=False)
+1 -1
View File
@@ -13,7 +13,7 @@ VALID_KEY_CHARS = string.ascii_lowercase + string.digits
class Session(AbstractBaseSession):
device_info = models.JSONField(null=True, blank=True, default=None)
session_key = models.CharField(max_length=128, primary_key=True)
user_id = models.CharField(null=True, max_length=50)
user_id = models.CharField(null=True, max_length=50, db_index=True)
@classmethod
def get_session_store_class(cls):
+1
View File
@@ -58,6 +58,7 @@ class IssueView(WorkspaceBaseModel):
filters = models.JSONField(default=dict)
display_filters = models.JSONField(default=get_default_display_filters)
display_properties = models.JSONField(default=get_default_display_properties)
rich_filters = models.JSONField(default=dict)
access = models.PositiveSmallIntegerField(
default=1, choices=((0, "Private"), (1, "Public"))
)
+3 -1
View File
@@ -155,7 +155,8 @@ class Workspace(BaseModel):
self, using: Optional[str] = None, soft: bool = True, *args: Any, **kwargs: Any
):
"""
Override the delete method to append epoch timestamp to the slug when soft deleting.
Override the delete method to append epoch timestamp to the slug when
soft deleting.
Args:
using: The database alias to use for the deletion.
@@ -332,6 +333,7 @@ class WorkspaceUserProperties(BaseModel):
filters = models.JSONField(default=get_default_filters)
display_filters = models.JSONField(default=get_default_display_filters)
display_properties = models.JSONField(default=get_default_display_properties)
rich_filters = models.JSONField(default=dict)
class Meta:
unique_together = ["workspace", "user", "deleted_at"]
+2 -1
View File
@@ -98,7 +98,8 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
from django.db import connection
print(
f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}"
f"{request.method} - {request.get_full_path()} of Queries: "
f"{len(connection.queries)}"
)
return response
@@ -167,7 +167,8 @@ class EmailCredentialCheckEndpoint(BaseAPIView):
except ConnectionError:
return Response(
{
"error": "Network connection error. Please check your internet connection."
"error": "Network connection error. Please check your "
"internet connection."
},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -0,0 +1,18 @@
# Generated by Django 4.2.22 on 2025-09-11 08:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("license", "0005_rename_product_instance_edition_and_more"),
]
operations = [
migrations.AddField(
model_name="instance",
name="is_current_version_deprecated",
field=models.BooleanField(default=False),
),
]
@@ -38,6 +38,8 @@ class Instance(BaseModel):
is_signup_screen_visited = models.BooleanField(default=False)
is_verified = models.BooleanField(default=False)
is_test = models.BooleanField(default=False)
# field for validating if the current version is deprecated
is_current_version_deprecated = models.BooleanField(default=False)
class Meta:
verbose_name = "Instance"
+2 -1
View File
@@ -155,7 +155,8 @@ class ReadReplicaRoutingMiddleware:
# provides extra protection specifically for view exceptions
clear_read_replica_context()
logger.debug(
f"Cleaned up read replica context due to exception: {type(exception).__name__}"
f"Cleaned up read replica context due to exception: "
f"{type(exception).__name__}"
)
# Return None to let the exception continue propagating
+4 -2
View File
@@ -22,7 +22,8 @@ class RequestLoggerMiddleware:
def _should_log_route(self, request: Request | HttpRequest) -> bool:
"""
Determines whether a route should be logged based on the request and status code.
Determines whether a route should be logged based on the request and
status code.
"""
# Don't log health checks
if request.path == "/" and request.method == "GET":
@@ -136,6 +137,7 @@ class APITokenLogMiddleware:
except Exception as e:
api_logger.exception(e)
# If the token does not exist, you can decide whether to log this as an invalid attempt
# If the token does not exist, you can decide whether to log
# this as an invalid attempt
return None
+10 -2
View File
@@ -74,7 +74,9 @@ REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
"DEFAULT_FILTER_BACKENDS": ("django_filters.rest_framework.DjangoFilterBackend",),
"EXCEPTION_HANDLER": "plane.authentication.adapter.exception.auth_exception_handler",
"EXCEPTION_HANDLER": (
"plane.authentication.adapter.exception.auth_exception_handler"
),
# Preserve original Django URL parameter names (pk) instead of converting to 'id'
"SCHEMA_COERCE_PATH_PK": False,
}
@@ -199,7 +201,9 @@ else:
# Password validations
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
"NAME": (
"django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
)
},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
@@ -465,3 +469,7 @@ if ENABLE_DRF_SPECTACULAR:
REST_FRAMEWORK["DEFAULT_SCHEMA_CLASS"] = "drf_spectacular.openapi.AutoSchema"
INSTALLED_APPS.append("drf_spectacular")
from .openapi import SPECTACULAR_SETTINGS # noqa: F401
# MongoDB Settings
MONGO_DB_URL = os.environ.get("MONGO_DB_URL", False)
MONGO_DB_DATABASE = os.environ.get("MONGO_DB_DATABASE", False)
+13 -5
View File
@@ -19,7 +19,8 @@ class MongoConnection:
"""
A singleton class that manages MongoDB connections.
This class ensures only one MongoDB connection is maintained throughout the application.
This class ensures only one MongoDB connection is maintained throughout the
application.
It provides methods to access the MongoDB client, database, and collections.
Attributes:
@@ -47,7 +48,8 @@ class MongoConnection:
if not mongo_url or not mongo_db_database:
logger.warning(
"MongoDB connection parameters not configured. MongoDB functionality will be disabled."
"MongoDB connection parameters not configured. "
"MongoDB functionality will be disabled."
)
return cls._instance
@@ -59,7 +61,8 @@ class MongoConnection:
logger.info("MongoDB connection established successfully")
except Exception as e:
logger.warning(
f"Failed to initialize MongoDB connection: {str(e)}. MongoDB functionality will be disabled."
f"Failed to initialize MongoDB connection: {str(e)}. "
f"MongoDB functionality will be disabled."
)
return cls._instance
@@ -96,13 +99,15 @@ class MongoConnection:
collection_name (str): The name of the collection to retrieve
Returns:
Optional[Collection]: The MongoDB collection instance or None if not configured
Optional[Collection]: The MongoDB collection instance or None if
not configured
"""
try:
db = cls.get_db()
if db is None:
logger.warning(
f"Cannot access collection '{collection_name}': MongoDB not configured"
f"Cannot access collection '{collection_name}': "
f"MongoDB not configured"
)
return None
return db[collection_name]
@@ -118,4 +123,7 @@ class MongoConnection:
Returns:
bool: True if MongoDB is configured and connected, False otherwise
"""
if cls._client is None:
cls._instance = cls()
return cls._client is not None and cls._db is not None
+69 -42
View File
@@ -52,14 +52,16 @@ SPECTACULAR_SETTINGS = {
"name": "Assets",
"description": (
"**File Upload & Presigned URLs**\n\n"
"Generate presigned URLs for direct file uploads to cloud storage. Handle user avatars, "
"cover images, and generic project assets with secure upload workflows.\n\n"
"Generate presigned URLs for direct file uploads to cloud storage. "
"Handle user avatars, cover images, and generic project assets with "
"secure upload workflows.\n\n"
"*Key Features:*\n"
"- Generate presigned URLs for S3 uploads\n"
"- Support for user avatars and cover images\n"
"- Generic asset upload for projects\n"
"- File validation and size limits\n\n"
"*Use Cases:* User profile images, project file uploads, secure direct-to-cloud uploads."
"*Use Cases:* User profile images, project file uploads, "
"secure direct-to-cloud uploads."
),
},
# Project Organization
@@ -67,14 +69,16 @@ SPECTACULAR_SETTINGS = {
"name": "Cycles",
"description": (
"**Sprint & Development Cycles**\n\n"
"Create and manage development cycles (sprints) to organize work into time-boxed iterations. "
"Track progress, assign work items, and monitor team velocity.\n\n"
"Create and manage development cycles (sprints) to organize work into "
"time-boxed iterations. Track progress, assign work items, and monitor "
"team velocity.\n\n"
"*Key Features:*\n"
"- Create and configure development cycles\n"
"- Assign work items to cycles\n"
"- Track cycle progress and completion\n"
"- Generate cycle analytics and reports\n\n"
"*Use Cases:* Sprint planning, iterative development, progress tracking, team velocity."
"*Use Cases:* Sprint planning, iterative development, "
"progress tracking, team velocity."
),
},
# System Features
@@ -82,14 +86,16 @@ SPECTACULAR_SETTINGS = {
"name": "Intake",
"description": (
"**Work Item Intake Queue**\n\n"
"Manage incoming work items through a dedicated intake queue for triage and review. "
"Submit, update, and process work items before they enter the main project workflow.\n\n"
"Manage incoming work items through a dedicated intake queue "
"for triage and review. Submit, update, and process work items "
"before they enter the main project workflow.\n\n"
"*Key Features:*\n"
"- Submit work items to intake queue\n"
"- Review and triage incoming work items\n"
"- Update intake work item status and properties\n"
"- Accept, reject, or modify work items before approval\n\n"
"*Use Cases:* Work item triage, external submissions, quality review, approval workflows."
"*Use Cases:* Work item triage, external submissions, quality review, "
"approval workflows."
),
},
# Project Organization
@@ -97,14 +103,16 @@ SPECTACULAR_SETTINGS = {
"name": "Labels",
"description": (
"**Labels & Tags**\n\n"
"Create and manage labels to categorize and organize work items. Use color-coded labels "
"for easy identification, filtering, and project organization.\n\n"
"Create and manage labels to categorize and organize work items. "
"Use color-coded labels for easy identification, filtering, and "
"project organization.\n\n"
"*Key Features:*\n"
"- Create custom labels with colors and descriptions\n"
"- Apply labels to work items for categorization\n"
"- Filter and search by labels\n"
"- Organize labels across projects\n\n"
"*Use Cases:* Priority marking, feature categorization, bug classification, team organization."
"*Use Cases:* Priority marking, feature categorization, "
"bug classification, team organization."
),
},
# Team & User Management
@@ -112,14 +120,16 @@ SPECTACULAR_SETTINGS = {
"name": "Members",
"description": (
"**Team Member Management**\n\n"
"Manage team members, roles, and permissions within projects and workspaces. "
"Control access levels and track member participation.\n\n"
"Manage team members, roles, and permissions within projects "
"and workspaces. Control access levels and track member "
"participation.\n\n"
"*Key Features:*\n"
"- Invite and manage team members\n"
"- Assign roles and permissions\n"
"- Control project and workspace access\n"
"- Track member activity and participation\n\n"
"*Use Cases:* Team setup, access control, role management, collaboration."
"*Use Cases:* Team setup, access control, role management, "
"collaboration."
),
},
# Project Organization
@@ -127,14 +137,16 @@ SPECTACULAR_SETTINGS = {
"name": "Modules",
"description": (
"**Feature Modules**\n\n"
"Group related work items into modules for better organization and tracking. "
"Plan features, track progress, and manage deliverables at a higher level.\n\n"
"Group related work items into modules for better organization "
"and tracking. Plan features, track progress, and manage "
"deliverables at a higher level.\n\n"
"*Key Features:*\n"
"- Create and organize feature modules\n"
"- Group work items by module\n"
"- Track module progress and completion\n"
"- Manage module leads and assignments\n\n"
"*Use Cases:* Feature planning, release organization, progress tracking, team coordination."
"*Use Cases:* Feature planning, release organization, "
"progress tracking, team coordination."
),
},
# Core Project Management
@@ -142,14 +154,16 @@ SPECTACULAR_SETTINGS = {
"name": "Projects",
"description": (
"**Project Management**\n\n"
"Create and manage projects to organize your development work. Configure project settings, "
"manage team access, and control project visibility.\n\n"
"Create and manage projects to organize your development work. "
"Configure project settings, manage team access, and control "
"project visibility.\n\n"
"*Key Features:*\n"
"- Create, update, and delete projects\n"
"- Configure project settings and preferences\n"
"- Manage team access and permissions\n"
"- Control project visibility and sharing\n\n"
"*Use Cases:* Project setup, team collaboration, access control, project configuration."
"*Use Cases:* Project setup, team collaboration, access "
"control, project configuration."
),
},
# Project Organization
@@ -157,14 +171,16 @@ SPECTACULAR_SETTINGS = {
"name": "States",
"description": (
"**Workflow States**\n\n"
"Define custom workflow states for work items to match your team's process. "
"Configure state transitions and track work item progress through different stages.\n\n"
"Define custom workflow states for work items to match your "
"team's process. Configure state transitions and track work "
"item progress through different stages.\n\n"
"*Key Features:*\n"
"- Create custom workflow states\n"
"- Configure state transitions and rules\n"
"- Track work item progress through states\n"
"- Set state-based permissions and automation\n\n"
"*Use Cases:* Custom workflows, status tracking, process automation, progress monitoring."
"*Use Cases:* Custom workflows, status tracking, process "
"automation, progress monitoring."
),
},
# Team & User Management
@@ -172,14 +188,15 @@ SPECTACULAR_SETTINGS = {
"name": "Users",
"description": (
"**Current User Information**\n\n"
"Get information about the currently authenticated user including profile details "
"and account settings.\n\n"
"Get information about the currently authenticated user "
"including profile details and account settings.\n\n"
"*Key Features:*\n"
"- Retrieve current user profile\n"
"- Access user account information\n"
"- View user preferences and settings\n"
"- Get authentication context\n\n"
"*Use Cases:* Profile display, user context, account information, authentication status."
"*Use Cases:* Profile display, user context, account "
"information, authentication status."
),
},
# Work Item Management
@@ -187,70 +204,80 @@ SPECTACULAR_SETTINGS = {
"name": "Work Item Activity",
"description": (
"**Activity History & Search**\n\n"
"View activity history and search for work items across the workspace. "
"Get detailed activity logs and find work items using text search.\n\n"
"View activity history and search for work items across the "
"workspace. Get detailed activity logs and find work items "
"using text search.\n\n"
"*Key Features:*\n"
"- View work item activity history\n"
"- Search work items across workspace\n"
"- Track changes and modifications\n"
"- Filter search results by project\n\n"
"*Use Cases:* Activity tracking, work item discovery, change history, workspace search."
"*Use Cases:* Activity tracking, work item discovery, change "
"history, workspace search."
),
},
{
"name": "Work Item Attachments",
"description": (
"**Work Item File Attachments**\n\n"
"Generate presigned URLs for uploading files directly to specific work items. "
"Upload and manage attachments associated with work items.\n\n"
"Generate presigned URLs for uploading files directly to "
"specific work items. Upload and manage attachments "
"associated with work items.\n\n"
"*Key Features:*\n"
"- Generate presigned URLs for work item attachments\n"
"- Upload files directly to work items\n"
"- Retrieve and manage attachment metadata\n"
"- Delete attachments from work items\n\n"
"*Use Cases:* Screenshots, error logs, design files, supporting documents."
"*Use Cases:* Screenshots, error logs, design files, "
"supporting documents."
),
},
{
"name": "Work Item Comments",
"description": (
"**Comments & Discussions**\n\n"
"Add comments and discussions to work items for team collaboration. "
"Support threaded conversations, mentions, and rich text formatting.\n\n"
"Add comments and discussions to work items for team "
"collaboration. Support threaded conversations, mentions, and "
"rich text formatting.\n\n"
"*Key Features:*\n"
"- Add comments to work items\n"
"- Thread conversations and replies\n"
"- Mention users and trigger notifications\n"
"- Rich text and markdown support\n\n"
"*Use Cases:* Team discussions, progress updates, code reviews, decision tracking."
"*Use Cases:* Team discussions, progress updates, code "
"reviews, decision tracking."
),
},
{
"name": "Work Item Links",
"description": (
"**External Links & References**\n\n"
"Link work items to external resources like documentation, repositories, or design files. "
"Maintain connections between work items and external systems.\n\n"
"Link work items to external resources like documentation, "
"repositories, or design files. Maintain connections between "
"work items and external systems.\n\n"
"*Key Features:*\n"
"- Add external URL links to work items\n"
"- Validate and preview linked resources\n"
"- Organize links by type and category\n"
"- Track link usage and access\n\n"
"*Use Cases:* Documentation links, repository connections, design references, external tools."
"*Use Cases:* Documentation links, repository connections, "
"design references, external tools."
),
},
{
"name": "Work Items",
"description": (
"**Work Items & Tasks**\n\n"
"Create and manage work items like tasks, bugs, features, and user stories. "
"The core entities for tracking work in your projects.\n\n"
"Create and manage work items like tasks, bugs, features, and "
"user stories. The core entities for tracking work in your "
"projects.\n\n"
"*Key Features:*\n"
"- Create, update, and manage work items\n"
"- Assign to team members and set priorities\n"
"- Track progress through workflow states\n"
"- Set due dates, estimates, and relationships\n\n"
"*Use Cases:* Bug tracking, task management, feature development, sprint planning."
"*Use Cases:* Bug tracking, task management, feature "
"development, sprint planning."
),
},
],
+2 -1
View File
@@ -22,7 +22,8 @@ class DynamicBaseSerializer(BaseSerializer):
"""
Adjust the serializer's fields based on the provided 'fields' list.
:param fields: List or dictionary specifying which fields to include in the serializer.
:param fields: List or dictionary specifying which fields to include
in the serializer.
:return: The updated fields for the serializer.
"""
# Check each field_name in the provided fields.
+12 -9
View File
@@ -30,7 +30,6 @@ from plane.db.models import (
)
from plane.utils.content_validator import (
validate_html_content,
validate_json_content,
validate_binary_data,
)
@@ -290,20 +289,24 @@ class IssueCreateSerializer(BaseSerializer):
raise serializers.ValidationError("Start date cannot exceed target date")
# Validate description content for security
if "description" in data and data["description"]:
is_valid, error_msg = validate_json_content(data["description"])
if not is_valid:
raise serializers.ValidationError({"description": error_msg})
if "description_html" in data and data["description_html"]:
is_valid, error_msg = validate_html_content(data["description_html"])
is_valid, error_msg, sanitized_html = validate_html_content(
data["description_html"]
)
if not is_valid:
raise serializers.ValidationError({"description_html": error_msg})
raise serializers.ValidationError(
{"error": "html content is not valid"}
)
# Update the data with sanitized HTML if available
if sanitized_html is not None:
data["description_html"] = sanitized_html
if "description_binary" in data and data["description_binary"]:
is_valid, error_msg = validate_binary_data(data["description_binary"])
if not is_valid:
raise serializers.ValidationError({"description_binary": error_msg})
raise serializers.ValidationError(
{"description_binary": "Invalid binary data"}
)
return data
+2 -1
View File
@@ -96,7 +96,8 @@ class EntityAssetEndpoint(BaseAPIView):
if type not in allowed_types:
return Response(
{
"error": "Invalid file type. Only JPEG, PNG, WebP, JPG and GIF files are allowed.",
"error": "Invalid file type. Only JPEG, PNG, WebP, JPG and "
"GIF files are allowed.",
"status": False,
},
status=status.HTTP_400_BAD_REQUEST,

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