Compare commits

..
Author SHA1 Message Date
NarayanBavisetti 01e72c1dfb fix: event tracking 2023-11-20 13:50:24 +05:30
Bavisetti NarayanandGitHub 51e17643a2 fix: file structuring (#2797)
* fix: file structure changes

* fix: pages update

* fix: license imports changed
2023-11-20 11:59:20 +05:30
NikhilandGitHub 4c2074b6ff dev: environment settings (#2794)
* dev: update environment configuration

* dev: update the takeoff script for instance registration
2023-11-19 01:48:05 +05:30
c9ffc9465f fix: Labels delete & reordering (#2729)
* fix: Labels reordering inconsistency

* fix: Delete child labels

* feat: multi-select while grouping labels

* refactor: label sorting in mobx computed function

* feat: drag & drop label grouping, un-grouping

* chore: removed label select modal

* fix: moving labels from project store to project label store

* fix: typo changes and build tree function added

* labels feature

* disable dropping group into a group

* fix build errors

* fix more issues

* chore: added combining state UI, fixed scroll issue for label groups

* chore: group icon for label groups

* fix: group cannot be dropped in another group

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: rahulramesha <rahulramesham@gmail.com>
Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-11-19 01:46:11 +05:30
Bavisetti NarayanandGitHub 2b6c489513 feat: v3 endpoint for module and cycle (#2786)
* feat: v3 endpoint for module and cycle

* fix: removed the str
2023-11-18 16:30:35 +05:30
M. PalanikannanandGitHub 0c63f21718 fix: Task List Behaviour in Editor (#2789)
* better variable names and comments

* drag drop migrated

* custom horizontal rule created

* init transaction hijack

* fixed code block with better contrast, keyboard tripple enter press disabled and syntax highlighting

* fixed link selector closing on open behaviour

* added better keymaps and syntax highlights

* made drag and drop working for code blocks

* fixed drag drop for code blocks

* moved drag drop only to rich text editor

* fixed drag and drop only for description

* enabled drag handles for peek overview and main issues

* got images to old state

* fixed task lists to be smaller

* removed validate image functions and uncessary imports

* table icons svg attributes fixed

* custom list keymap extension added

* more uncessary imports of validate image removed

* removed console logs

* fixed drag-handle styles

* space styles updated for the editor

* removed showing quotes from blockquotes

* removed validateImage for now

* added better comments and improved redundant renders

* removed uncessary console logs

* created util for creating the drag handle element

* fixed file names
2023-11-18 16:20:35 +05:30
NikhilandGitHub a987df38f4 chore: user onboarding workflow (#2791) 2023-11-18 16:18:06 +05:30
878707f444 feat: Instance Registration and Configuration (#2793)
* dev: remove default user

* dev: initiate licensing

* dev: remove migration file 0046

* feat: self hosted licensing initialize

* dev: instance licenses

* dev: change license response structure

* dev: add default properties and issue mention migration

* dev: reset migrations

* dev: instance configuration

* dev: instance configuration migration

* dev: update instance configuration model to take null and empty values

* dev: instance configuration variables

* dev: set default values

* dev: update instance configuration load

* dev: email configuration settings moved to database

* dev: instance configuration on instance bootup

* dev: auto instance registration script

* dev: instance admin

* dev: enable instance configuration and instance admin roles

* dev: instance owner fix

* dev: instance configuration values

* dev: fix instance permissions and serializer

* dev: fix email senders

* dev: remove deprecated variables

* dev: fix current site domain registration

* dev: update cors setup and local settings

* dev: migrate instance registration and configuration to manage commands

* dev: check email validity

* dev: update script to use manage command

* dev: default bucket creation script

* dev: instance admin routes and initial set of screens

* dev: admin api to check if the current user is admin

* dev: instance admin unique constraints

* dev: check magic link login

* dev: fix email sending for ssl

* dev: create instance activation route if the instance is not activated during startup

* dev: removed DJANGO_SETTINGS_MODULE from environment files and deleted auto bucket create script

* dev: environment configuration for backend

* dev: fix access token variable error

* feat: Instance Admin Panel: General Settings (#2792)

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
2023-11-18 16:17:01 +05:30
M. PalanikannanandGitHub 9369ee5008 [feat]: Drag and Drop Handles for all Data Structures (#2745)
* better variable names and comments

* drag drop migrated

* custom horizontal rule created

* init transaction hijack

* fixed code block with better contrast, keyboard tripple enter press disabled and syntax highlighting

* fixed link selector closing on open behaviour

* added better keymaps and syntax highlights

* made drag and drop working for code blocks

* fixed drag drop for code blocks

* moved drag drop only to rich text editor

* fixed drag and drop only for description

* enabled drag handles for peek overview and main issues

* got images to old state
2023-11-17 12:29:30 +05:30
0a88db975a dev: Self Hosting with private repo fixes (#2787)
* fixes to self hosting

* self hosting fixes

* removed .temp

* wip

* wip

* self install private repo

* folder change

* fix

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2023-11-17 11:51:54 +05:30
dd60dec887 Dev/mg selfhosting fix (#2782)
* fixes to self hosting

* self hosting fixes

* removed .temp

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2023-11-16 14:38:55 +05:30
Bavisetti NarayanandGitHub 0c1097592e fix: pages revamping (#2760)
* fix: page transaction model

* fix: page transaction model

* fix: migration and optimisation

* fix: back migration of page blocks

* fix: added issue embed

* fix: migration fixes

* fix: resolved changes
2023-11-16 14:38:12 +05:30
Anmol Singh BhatiaandGitHub bed66235f2 style: workspace sidebar dropdown improvement (#2783) 2023-11-16 14:11:33 +05:30
NikhilandGitHub 26b1e9d5f1 dev: squashed migrations (#2779)
* dev: migration squash

* dev: migrations squashed for apis and webhooks

* dev: packages updated and  move dj-database-url for local settings

* dev: update package changes
2023-11-15 17:15:02 +05:30
+1 79347ec62b feat: api webhooks (#2543)
* dev: initiate external apis

* dev: external api

* dev: external public api implementation

* dev: add prefix to all api tokens

* dev: flag to enable disable api token api access

* dev: webhook model create and apis

* dev: webhook settings

* fix: webhook logs

* chore: removed drf spectacular

* dev: remove retry_count and fix api logging for get requests

* dev: refactor webhook logic

* fix: celery retry mechanism

* chore: event and action change

* chore: migrations changes

* dev: proxy setup for apis

* chore: changed retry time and cleanup

* chore: added issue comment and inbox issue api endpoints

* fix: migration files

* fix: added env variables

* fix: removed issue attachment from proxy

* fix: added new migration file

* fix: restricted wehbook access

* chore: changed urls

* chore: fixed porject serializer

* fix: set expire for api token

* fix: retrive endpoint for api token

* feat: Api Token screens & api integration

* dev: webhook endpoint changes

* dev: add fields for webhook updates

* feat: Download Api secret key

* chore: removed BASE API URL

* feat: revoke token access

* dev: migration fixes

* feat: workspace webhooks (#2748)

* feat: workspace webhook store, services integeration and rendered webhook list and create

* chore: handled webhook update and rengenerate token in workspace webhooks

* feat: regenerate key and delete functionality

---------

Co-authored-by: Ramesh Kumar <rameshkumar@rameshs-MacBook-Pro.local>
Co-authored-by: gurusainath <gurusainath007@gmail.com>
Co-authored-by: Ramesh Kumar Chandra <rameshkumar2299@gmail.com>

* fix: url validation added

* fix: seperated env for webhook and api

* Web hooks refactoring

* add show option for generated hook key

* Api token restructure

* webhook minor fixes

* fix build errors

* chore: improvements in file structring

* dev: rate limiting the open apis

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: LAKHAN BAHETI <lakhanbaheti9@gmail.com>
Co-authored-by: rahulramesha <71900764+rahulramesha@users.noreply.github.com>
Co-authored-by: Ramesh Kumar <rameshkumar@rameshs-MacBook-Pro.local>
Co-authored-by: gurusainath <gurusainath007@gmail.com>
Co-authored-by: Ramesh Kumar Chandra <rameshkumar2299@gmail.com>
Co-authored-by: Nikhil <118773738+pablohashescobar@users.noreply.github.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: rahulramesha <rahulramesham@gmail.com>
2023-11-15 15:56:57 +05:30
NikhilandGitHub 7b965179d8 dev: update bucket script to make the bucket public (#2767)
* dev: update bucket script to make the bucket public

* dev: remove auto bucket script from docker compose
2023-11-15 15:56:08 +05:30
NikhilandGitHub fc51ffc589 chore: user workflow (#2762)
* dev: workspace member deactivation and leave endpoints and filters

* dev: deactivated for project members

* dev: project members leave

* dev: project member check on workspace deactivation

* dev: project member queryset update and remove leave project endpoint

* dev: rename is_deactivated to is_active and user deactivation apis

* dev: check if the user is already part of workspace then make them active

* dev: workspace and project save

* dev: update project members to make them active

* dev: project invitation

* dev: automatic user workspace and project member create when user sign in/up

* dev: fix member invites

* dev: rename deactivation variable

* dev: update project member invitation

* dev: additional permission layer for workspace

* dev: update the url for  workspace invitations

* dev: remove invitation urls from users

* dev: cleanup workspace invitation workflow

* dev: workspace and project invitation
2023-11-15 15:53:16 +05:30
sabith-tuandGitHub 96f6e37cc5 fix: Delete estimate popup is not closing automatically (#2777) 2023-11-15 14:08:52 +05:30
NikhilandGitHub 29774ce84a dev: API settings (#2594)
* dev: update settings file structure and added extra settings for CORS

* dev: remove WEB_URL variable and add celery integration for sentry

* dev: aws and minio settings

* dev: add cors origins to env

* dev: update settings
2023-11-15 12:31:52 +05:30
8cbe9c26fc enhancement: label sort order (#2763)
* chore: label sort ordering

* dev: ordering

* fix: sort order

* fix: save of labels

* dev: remove ordering by name

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2023-11-15 12:25:44 +05:30
Prateek ShouryaandGitHub 7f42566207 Fix: Custom menu item not automatically closing, affecting delete popup behavior. (#2771) 2023-11-14 23:05:30 +05:30
Ankush DeshmukhandGitHub b60237b676 Standarding priority icons across the platform (#2776) 2023-11-14 20:52:43 +05:30
Prateek ShouryaandGitHub 1fe09d369f style: text overflow fix and border color update (#2769)
* style: fix text overflow in:
* Issue activity
* Cycle and Module Select in Create Issue form
* Delete Module modal
* Join Project modal

* style: update assignee select border as per design.
2023-11-14 18:34:51 +05:30
Dakshesh JainandGitHub b7757c6b1a fix: bugs (#2761)
* fix: semicolon on estimate settings page

* refactor: project settings automations store implementation

* fix: active cycle stuck on infinite loading

* fix: removed delete project option from sidebar

* fix: discloser not opening when navigating to project

* fix: clear filter not working & filter appearing even if nothing is selected

* refactor: select label store implementation

* refactor: select state store implementation
2023-11-14 18:33:01 +05:30
Anmol Singh BhatiaandGitHub 1a25bacce1 style: create update view modal consistency (#2775) 2023-11-14 18:30:10 +05:30
Anmol Singh BhatiaandGitHub 6797df239d chore: no lead option added in lead select dropdown (#2774) 2023-11-14 18:29:39 +05:30
Anmol Singh BhatiaandGitHub 43e7c10eb7 chore: spreadsheet layout column responsiveness (#2768) 2023-11-14 18:28:49 +05:30
Anmol Singh BhatiaandGitHub bdc9c9c2a8 chore: create update issue modal improvement (#2765) 2023-11-14 18:28:15 +05:30
Anmol Singh BhatiaandGitHub f0c72bf249 fix: breadcrumb project icon improvement (#2764) 2023-11-14 18:27:47 +05:30
sabith-tuandGitHub a8904bfc48 style: ui fixes for pages and views (#2770) 2023-11-14 18:26:50 +05:30
NikhilandGitHub b31041726b dev: create bucket through application (#2720) 2023-11-13 15:57:19 +05:30
Prateek ShouryaandGitHub e6f947ad90 style: ui improvements and bug fixes (#2758)
* style: add transition to favorite projects dropdown.

* style: update project integration settings borders.

* style: fix text overflow issue in project views.

* fix: issue with non-functional cancel button in leave project modal.
2023-11-13 14:42:45 +05:30
Dakshesh JainandGitHub 7963993171 fix: workspace settings bugs (#2743)
* fix: double layout in exports

* fix: typo in jira email address section

* fix: workspace members not mutating

* fix: removed un-used variable

* fix: workspace members can't be filtered using email

* fix: autocomplete in workspace delete

* fix: autocomplete in project delete modal

* fix: update member function in store

* fix: sidebar link not active when in github/jira

* style: margin top & icon inconsistency

* fix: typo in create workspace

* fix: workspace leave flow

* fix: redirection to delete issue

* fix: autocomplete off in jira api token

* refactor: reduced api call, added optional chaining & removed variable with low scope
2023-11-13 13:34:05 +05:30
Anmol Singh BhatiaandGitHub 00e61a8753 fix: peek overview comment ordering and comment icon alignment fix (#2753) 2023-11-10 18:45:41 +05:30
Anmol Singh BhatiaandGitHub 733fed76cc fix: cycle card title responsiveness added (#2752) 2023-11-10 18:43:48 +05:30
Anmol Singh BhatiaandGitHub e78dd4b1c0 fix: app sidebar dropdown fix (#2751) 2023-11-10 18:43:16 +05:30
Anmol Singh BhatiaandGitHub d479781fce style: header consistency (#2750) 2023-11-10 18:30:43 +05:30
Bavisetti NarayanandGitHub c449b46bf4 fix: added external folder in urls (#2749) 2023-11-10 16:00:55 +05:30
Prateek ShouryaandGitHub fd6430c3e3 style: Update modal appearance for UI consistency (#2747) 2023-11-10 15:48:34 +05:30
Ramesh Kumar ChandraandGitHub 6f580ce2d9 fix: project settings layout render in export (#2746) 2023-11-10 13:07:18 +05:30
Ankush DeshmukhandGitHub 2748133bd0 Fix: Show Priority icon in custom analytics table. (#2744) 2023-11-10 13:06:23 +05:30
Aaryan KhandelwalandGitHub 884b219508 refactor: cycles store (#2716)
* refactor: cycles store

* refactor: active cycle details
2023-11-09 18:37:45 +05:30
Anmol Singh BhatiaandGitHub 162faf8339 fix: date select tooltip fix (#2740) 2023-11-09 18:29:45 +05:30
Anmol Singh BhatiaandGitHub c291ff05ee fix: fliter list item clear button alignment fix (#2741) 2023-11-09 18:27:19 +05:30
NikhilandGitHub 446981422e feat: issues v2 endpoint (#2713)
* feat: issue v2 listing endpoint

* dev: issues v3 endpoint

* dev: add permission in the grouped endpoint

* dev: update grouped endpoint
2023-11-09 18:24:26 +05:30
Bavisetti NarayanandGitHub 630e21b954 fix: favourite cycle and modules displayed at top (#2719) 2023-11-09 18:22:38 +05:30
894ffb6c21 fix: mention notification (#2670)
* fix: mention notification

* feat: updated mentions for comments in the notification background task

* feat: added subscription for issue_comment_mentions as well

* fix: removed the print statement

* fix: double notification popup for mentioned assignees

* fix: added issue subscriber

* fix: removed creator for subscribed

* fix: creator will not be subscribed to issue

* fix: double notification removed

---------

Co-authored-by: Henit Chobisa <chobisa.henit@gmail.com>
2023-11-09 18:22:13 +05:30
NikhilandGitHub 515dba02d3 chore: configuration add tracker variables (#2709)
* chore: configuration add tracker variables

* dev: unsplash configuration
2023-11-09 18:20:03 +05:30
Dakshesh JainandGitHub 34bccd7e06 refactor: gantt sidebar (#2705)
* refactor: gantt sidebar

* fix: exception error

fix: file placement

* refactor: not passing sidebar block as props
2023-11-09 17:57:41 +05:30
sriram veeraghantaandGitHub 79df59f618 fix: spliting out the project members from project store and service (#2739) 2023-11-09 17:56:55 +05:30
Prateek ShouryaandGitHub 7676aab773 fix: UI improvements. (#2734)
* style: update check icon colors to match our design.

* fix: automatically focus input box in pages `add label` modal.
2023-11-09 17:37:45 +05:30
Prateek ShouryaandGitHub 8832d8e00e style: sidebar UI improvements (#2735)
* updated font weight and color as per designs.
* removed background color from workspace with logo.
* updated dropdown design.
2023-11-09 17:01:48 +05:30
d733a53ea6 fix: Add horizontal scroll bar to views (#2736)
* add errors for duplicate labels

* adding horizonatal scroll bar to views

---------

Co-authored-by: rahulramesha <rahul@appsmith.com>
2023-11-09 15:12:00 +05:30
Lakhan BahetiandGitHub 96862e06ef fix: cystom analytics bar graph index alignment (#2737) 2023-11-09 14:36:22 +05:30
sriram veeraghantaandGitHub a6567bbce4 fix: workspace members store added and implemented across the app (#2732)
* fix: minor changes

* fix: workspace members store added and implemnted across the app
2023-11-09 00:35:12 +05:30
NikhilandGitHub 556b2d2617 feat: state list endpoint (#2717)
* feat: state list endpoint

* dev: update states endpoint

* dev: mark default state endpoint
2023-11-08 22:38:53 +05:30
Lakhan BahetiandGitHub 10037222b6 fix: Tooltip content on assignee hover in all layouts (#2724)
* fix: Tooltip content on assignee hover in all layouts

* chore: comments added
2023-11-08 22:35:30 +05:30
sabith-tuandGitHub 931f9d288a fix: toast alert inconsistency (#2730) 2023-11-08 20:37:47 +05:30
sriram veeraghantaandGitHub 20fb79567f fix: project states fixes (#2731)
* fix: project states fixes

* fix: states fixes

* fix: formating all files
2023-11-08 20:31:46 +05:30
Ramesh Kumar ChandraandGitHub bd1a850f35 style: kanban card label overflow (#2722)
* chore: kanban card lable drop down items overflow

* style: kaban card label text overflow, tool tip, hover cursor

* style: label overflow in list layout
2023-11-08 18:12:36 +05:30
206f5744a3 [fix]: Error Handling for Images and Table Fix for Form Submissions in Editor (#2710)
* cancellable uploads and image limits with better error handling

* fixed table row/column picker behaviour on modals

* Merge branch 'rerender-debounce-editor-fix' into editor-draggable-nodes

* fix: added mention suggestions and highlights in `create-issue-modal`

* removed uncessary files

* solved lint error of trailing spaces

* added plane/ui dependency for tooltips

---------

Co-authored-by: Henit Chobisa <chobisa.henit@gmail.com>
2023-11-08 18:00:53 +05:30
sabith-tuandGitHub faaba45e59 fix: all issues values not changeable and assignee image not rendering (#2707)
* fix: all issues values are not changeable and assignee image not rendering

* chore: removed console log
2023-11-08 17:56:09 +05:30
sabith-tuandGitHub f8002852e0 fix: issue property height and peek view date picker border radius (#2726) 2023-11-08 17:55:28 +05:30
Lakhan BahetiandGitHub 2d71377722 fix: added empty project state when no project exists. (#2727)
* fix: added empty project state when no project exists

* fix: duplicate import
2023-11-08 17:54:59 +05:30
Lakhan BahetiandGitHub 6ebee05951 fix: unwanted go back button in onboarding step 2 (#2714) 2023-11-08 17:53:06 +05:30
Lakhan BahetiandGitHub 5a3bac998e fix: bug fixes and ui improvements (#2703)
* fix: gantt chart duration in decimal

* fix: Loading text instead Spinner in peek view

* fix: cycle more popover typo & icon overlapping

* fix: list layout properties alignment

* fix: project search empty state

* fix: calendar layout issue text overflow & redirection inconsistency

* style: urgent priority hover background color

* fix: Cycle issues kanban layout empty state missing

* style: custom snooze modal placeholder text color

* refactor: replaced unwanted anchor tag with div

* chore: removed empty state for cycle kanban layout
2023-11-08 17:52:34 +05:30
Anmol Singh BhatiaandGitHub 4096136b44 style: ui consistency and improvement (#2725)
* style: create/update issue modal properties ui improvement

* style: create update issue modal improvement

* style: modal ui consistency
2023-11-08 17:51:32 +05:30
Aaryan KhandelwalandGitHub 83e0c4ebbd chore: remove active ids from the MobX stores if not present in the route (#2681)
* chore: remove active ids if not present in the route

* refactor: set active id logic
2023-11-08 17:35:45 +05:30
Aaryan KhandelwalandGitHub df8bdfd5b9 fix: project automation settings flickering (#2680)
* fix: cycle and module sidebar z-index

* fix: project automation settings flickering
2023-11-08 17:34:42 +05:30
Ankush DeshmukhandGitHub da799b5a63 Fix: Render bar chart axis labels in lighter color when dark theme applied (#2721) 2023-11-08 17:34:09 +05:30
Dakshesh JainandGitHub 621d551c4a fix: project select validation (#2723) 2023-11-08 17:33:26 +05:30
Aaryan KhandelwalandGitHub 5a84ed279d refactor: replace keyboard events with command palette store (#2688) 2023-11-08 13:51:55 +05:30
Dakshesh JainandGitHub 53e7da08e4 fix: quick add not working for labels & assignee (#2689)
* fix: quick add not working for labels & assignee

* fix: build error on spreadsheet view
2023-11-08 12:28:02 +05:30
Anmol Singh BhatiaandGitHub 9f206331bc style: spinner component improvement (#2708) 2023-11-07 18:35:05 +05:30
b56d188a83 add errors for duplicate labels (#2706)
Co-authored-by: rahulramesha <rahul@appsmith.com>
2023-11-07 18:22:52 +05:30
Bavisetti NarayanandGitHub 8d3853b129 fix: issue draft delete functionality (#2696) 2023-11-07 18:19:32 +05:30
Bavisetti NarayanandGitHub 30d6235108 fix: add issues to cycles and modules (#2659)
* fix: able to add issue in cycle and module

* fix: issue activity message
2023-11-07 18:18:51 +05:30
Prateek ShouryaandGitHub 6e461dd8c3 style: update user profile button alignment. (#2695) 2023-11-07 18:17:44 +05:30
NikhilandGitHub 86379c51b7 dev: worker count (#2573)
* dev: workers count

* dev: update the worker count variable to GUNICORN_WORKERS
2023-11-07 18:05:38 +05:30
Manish GuptaandGitHub f7cc2eca36 dev: Modified the branch-build action yaml (#2704)
* cherrypicked code

* removed PUSH event
2023-11-07 17:41:26 +05:30
Prateek ShouryaandGitHub 63d1ad286b style: update font weight in project general setting section. (#2697) 2023-11-07 17:39:12 +05:30
Manish GuptaandGitHub 1412c1c94a dev: modified the branch wise build (#2702)
* cherrypicked branch build code

* trigger on pull request

* branch filter

* checking branch filter

* checking push

* checking push again

* code cleanup before PR
2023-11-07 17:23:32 +05:30
26de35bd8d fix: environment config changes in the API are replicated in web and space app (#2699)
* fix: envconfig type changes

* chore: configuration variables  (#2692)

* chore: update avatar group logic (#2672)

* chore: configuration variables

---------

* fix: replacing slack client id with env config

---------

Co-authored-by: Nikhil <118773738+pablohashescobar@users.noreply.github.com>
2023-11-07 17:17:10 +05:30
Aaryan KhandelwalandGitHub 1986c0dfd4 fix: state icon (#2678) 2023-11-07 16:15:34 +05:30
Anmol Singh BhatiaandGitHub 25f3a5b2e4 chore: peek overview improvement and bug fixes (#2683)
* style: issue peek overview improvement

* style: peek overview improvement

* fix: subscribe issue from peek overview fix and validation added

* fix: build error
2023-11-07 15:58:42 +05:30
Anmol Singh BhatiaandGitHub f30b16e9d8 chore: user profile issue improvement (#2679)
* fix: user profile filters z-index

* chore: user profile issue state group heading fix

* fix: build error
2023-11-07 15:58:19 +05:30
Anmol Singh BhatiaandGitHub 93d03f82b4 chore: spreadsheet layout improvement (#2677)
* style: spreadsheet column width fix

* style: spreadsheet label column styling

* chore: spreadsheet layout issue properties improvement

* fix: build error
2023-11-07 15:58:00 +05:30
Anmol Singh BhatiaandGitHub 98974fdc50 fix: cycle and module bug fixes and improvement (#2691)
* fix: cycle and module card issue count fix

* fix: cycle and module list progress icon fix

* fix: module card progress fix

* style: cycle & module empty date label updated

* fix: build error
2023-11-07 15:14:47 +05:30
sriram veeraghantaandGitHub 040563d148 fix: replacing jira importer image (#2685) 2023-11-07 14:35:04 +05:30
NikhilandGitHub 4de64f112f fix: slack project integration (#2684) 2023-11-07 14:34:30 +05:30
Ramesh Kumar ChandraandGitHub 0afb900678 fix: kanban card state name and drop down items text overflow (#2686) 2023-11-07 14:31:29 +05:30
Prateek ShouryaandGitHub baf17a109b style: update project description as per design. (#2682) 2023-11-07 13:13:14 +05:30
Prateek ShouryaandGitHub 37bf465fcd style: update border across workspace and project settings. (#2669)
* style: update border across workspace and project settings.

* update border width
2023-11-07 13:12:05 +05:30
Anmol Singh BhatiaandGitHub d8c96536f0 fix: bug fixes and ui improvement (#2674)
* chore: peekoverview edit permission updated

* chore: tab index added in create project modal

* chore: project card improvement

* style: avatar component improvement

* chore: create issue modal improvement

* style: global style sidebar border variable name fix
2023-11-06 21:08:01 +05:30
b372ccfdb3 fix: slack integration workflow (#2675)
* fix: slack integration workflow

* dev: add slack client id as configuration

* fix: clean up

* fix: added env to turbo

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2023-11-06 21:00:49 +05:30
guru_sainathandGitHub 984b36f45a fix: In kanban issues can be shifted between the column in order_by (#2676) 2023-11-06 21:00:36 +05:30
Aaryan KhandelwalandGitHub 46f307fed5 chore: update avatar group logic (#2672) 2023-11-06 20:43:54 +05:30
Aaryan KhandelwalandGitHub 1dce72cb3c style: updated layouts UI in the space app (#2671)
* style: updated layouts UI in space

* fix: build error
2023-11-06 20:43:34 +05:30
Aaryan KhandelwalandGitHub a6dea3af23 fix: render the estimate select if estimate is enabled for the project (#2663) 2023-11-06 20:43:10 +05:30
Henit ChobisaandGitHub 6eb0bf4785 Fix/mentions spaces fix (#2667)
* feat: add mentions store to the space project

* fix: added mentions highlights in read only comment cards

* feat: added mention highlights in richtexteditor in space app
2023-11-06 20:42:24 +05:30
13389d1b2b dev: On Demand Code Build for any branch (#2668)
* wip

* wip

* testing

* wip

* wip

* wip

* wip

* image push fix

* wip

* wip

* dynamic branch name and tag

* workflow_dispatch modified

* job splitting

* file sharing

* wip

* checking

* wip

* wip

* wip

* wip

* build fixes

* code upload download fixes

* image name change

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2023-11-06 19:05:20 +05:30
Aaryan KhandelwalandGitHub 742143766f fix: existing issues modal for cycle and module (#2664)
* fix: existing issues modal for cycle and module

* refactor: existing issues modal code

* fix: build errors
2023-11-06 16:30:09 +05:30
sriram veeraghantaandGitHub 1ed72c51df fix: package version fixes and mentions build error fixes (#2665) 2023-11-06 16:28:15 +05:30
Aaryan KhandelwalandGitHub a03e0c788f fix: notifications option in the sidebar menu not collapsing (#2662) 2023-11-06 14:53:26 +05:30
guru_sainathandGitHub 0c8a867565 fix: handled drag and drop issue, gantt hover issue for issue peek overview (#2660) 2023-11-06 13:52:33 +05:30
Aaryan KhandelwalandGitHub 3a07bb6060 refactor: removed unused packages (#2658) 2023-11-06 13:17:02 +05:30
Aaryan KhandelwalandGitHub bf48d93a25 fix: product tour modal bugs (#2657)
* fix: product tour

* style: product tour navigation buttons

* refactor: step logic
2023-11-06 13:06:00 +05:30
14ac885e55 [feat]: Extended Tables Support (#2596)
* migrated table to new project structure

* fixed range errors while deleting table nodes with no nodes below and removed console logs

* fixed css for rendering table menu

* removed old table menu

* added support for read only editors as well

* text-black removed

* added design colors

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2023-11-05 18:54:00 +05:30
Henit ChobisaandGitHub f0335751b3 fix: mentions enter error (#2646)
* fix: fixed readonly lite text editor not rendering highlights

* fix: removed enter extension in lite text editor
2023-11-04 01:57:57 +05:30
Anmol Singh BhatiaandGitHub 52395d0563 chore : dropdown loading state added and project card avatar fix (#2643)
* chore: project card avatar rendering fix

* chore: state, assignee and label dropdown loading state added
2023-11-04 01:56:23 +05:30
Dakshesh JainandGitHub ad558833af refactor: archive issue (#2628)
* dev: archived issue store

* dev: archived issue layouts and store binding

* dev: archived issue detail store

* dev: is read only

* fix: archived issue delete

* fix: build error
2023-11-03 20:20:05 +05:30
sriram veeraghantaandGitHub ff258c60fd fix: open changelog in new tab (#2645) 2023-11-03 19:49:02 +05:30
Bavisetti NarayanandGitHub db2a1b8033 fix: custom analytics graph display issue (#2637)
* chore: fixed custom analytics

* chore: typo changes
2023-11-03 19:23:35 +05:30
Dakshesh JainandGitHub 91878fb3dd fix: notification read and snooze bugs (#2639)
* fix: marking notification as read doesn't remove it from un-read list

* refactor: arranged imports

* fix: past snooze notifications coming in snooze tab
2023-11-03 19:21:35 +05:30
Lakhan BahetiandGitHub c233e6e3b6 style: custom analytics bar graph label overlapping (#2636)
* style: custom analytics bar graph label overlapping

fix: Bar graph avatar image rendering & tooltip

* fix: import
2023-11-03 19:18:24 +05:30
0d2c399555 style: quick add issue UI improvements in all the layouts (#2615)
* style: quick add issue UI improvements in all the layouts

* style: ui improvements

* style: quick add icon size

* chore: static sizes to tailwind classes

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-11-03 19:17:50 +05:30
Aaryan KhandelwalandGitHub 79cad16aba chore: update all layout selections (#2641) 2023-11-03 19:17:13 +05:30
Aaryan KhandelwalandGitHub 41e9d5d7e3 chore: added missing columns to the spreadsheet layout (#2640) 2023-11-03 19:15:09 +05:30
Aaryan KhandelwalandGitHub 992cf79031 chore: peek overview authorization (#2632)
* chore: peek overview authorization

* chore: comment access specifier validation
2023-11-03 19:13:10 +05:30
Aaryan KhandelwalandGitHub d48f13416f Update readme content (#2635) 2023-11-03 19:12:46 +05:30
Aaryan KhandelwalandGitHub cf19afa707 fix: string helper function (#2633) 2023-11-03 19:11:28 +05:30
sriram veeraghantaandGitHub cc26f604aa fix: next image fixes for selfhosted instances (#2642) 2023-11-03 19:09:40 +05:30
Anmol Singh BhatiaandGitHub 8919b724c5 chore: breadcrumbs ui revamp and refactor (#2634) 2023-11-03 18:01:49 +05:30
Anmol Singh BhatiaandGitHub 7eeac188d7 chore: peek overview improvement and bug fixes (#2627)
* chore: peekoverview issue properties text size fix

* chore: peekoverview icon updated and active view indicator added

* chore: peekoverview and issue sidebar improvement
2023-11-03 18:01:34 +05:30
Anmol Singh BhatiaandGitHub f639e467f8 refactor: replace ui components with plane ui components (#2626)
* refactor: replace button component with plane ui component and remove old button component

* refactor: replace dropdown component with plane ui component

* refactor: replace tooltip, input, textarea, spinner and loader component with plane ui component

* refactor: plane ui code refactor
2023-11-03 17:21:38 +05:30
Anmol Singh BhatiaandGitHub 737fea28c6 chore: refactor and improve project member settings (#2625)
* fix: project member setting improvement and refactor

* fix: typo fix in automations setting
2023-11-03 17:20:49 +05:30
4c1aee0cfc fix: resolve z-index and peek overview component bug (#2624)
* fix: resolved z-index issue on peek overview component

* fix: fix issue with peekover view in spreadsheet view

---------

Co-authored-by: gurusainath <gurusainath007@gmail.com>
2023-11-03 17:20:14 +05:30
guru_sainathandGitHub 1352c200dd fix: Project Rendering Error in Kanban Layout and Layout Rendering Fixes in Subscribed Profile Issues (#2629)
* fix: rendering projects error in kanabn layout in profile issues and resolved multiplr layout rendering in subscribed profile issues

* fix: implemented spinner loader in profile issues and remove logs in kanban layout
2023-11-03 13:17:52 +05:30
Aaryan KhandelwalandGitHub dd2ba2ec6f fix: slug field not working (#2622) 2023-11-03 13:17:01 +05:30
Aaryan KhandelwalandGitHub c66d76df26 chore: set sub group by to null if group by and sub group by are same (#2621) 2023-11-03 13:15:50 +05:30
Bavisetti NarayanandGitHub 7a11161cd0 dev: migrations for 0.14 (#2630)
* chore: migration files

* dev: deleted the old migration
2023-11-03 13:00:37 +05:30
260974b0de fix: user authentication on the index page (#2619)
* fix: user authentication on the index page

* fix: login redirection cleanup

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2023-11-03 12:42:43 +05:30
sriram veeraghantaandGitHub 5efc6993cd fix: Update analytics page layout fixes (#2623) 2023-11-03 00:09:13 +05:30
sriram veeraghantaandGitHub 3c884fd46e fix: implementing layouts using _app.tsx get layout method. (#2620)
* fix: implementing layouts in all pages

* fix: layout fixes, implemting using standard nextjs parctice
2023-11-02 23:57:44 +05:30
Anmol Singh BhatiaandGitHub a582021f2c fix: active cycle fix (#2618) 2023-11-02 22:17:10 +05:30
Bavisetti NarayanandGitHub caca2bb548 chore: bug fixes (#2609)
* chore: sub issue activity task

* fix: mentions and issue comment

* chore: added string for issue

* chore: changed sub issue id
2023-11-02 19:32:44 +05:30
da391064aa [FIX] Minor bug fixes in MentionList and MentionNode UI (#2600)
* fix: removed text color in peek view

* fix: fixed list view UI bugs and node view colors

* feat: update imports in suggestions for mentionSuggestion type

* fix: updated mention list css

* fix: updated mention node UI according to the design provided

* style: update the mentions dropdown UI

* style: mentioned users UI in the editor

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-11-02 19:31:25 +05:30
Aaryan KhandelwalandGitHub a9b72fa1d2 chore: implemented MobX in the onboarding screens (#2617)
* fix: wrap the onboarding route with the UserWrapper

* chore: implement mobx in the onboarding screens
2023-11-02 19:27:25 +05:30
Anmol Singh BhatiaandGitHub b0397dfd74 fix: module sidebar status select (#2614) 2023-11-02 18:48:57 +05:30
02d4d32f7a chore: Improved Handling of Empty Properties for Labels and Assignees (#2616)
* fix: show empty group

* chore: handled None values for lables and assignees in list and kanban layouts

---------

Co-authored-by: dakshesh14 <dakshesh.jain14@gmail.com>
2023-11-02 18:44:02 +05:30
Anmol Singh BhatiaandGitHub 43e42f1896 fix: project view item edit action fix (#2612) 2023-11-02 17:12:04 +05:30
Aaryan KhandelwalandGitHub d5fd69354e chore: removed unused hooks and components (#2611)
* chore: remove unused hooks

* chore: removed useProjectMembers hook

* chore: removed issue hooks

* fix: build errors
2023-11-02 17:11:33 +05:30
Aaryan KhandelwalandGitHub c987c6f308 fix: calendar layout not being rendered (#2610) 2023-11-02 17:08:48 +05:30
Anmol Singh BhatiaandGitHub 56e4152756 fix: select label dropdown fix for list view (#2608) 2023-11-02 16:28:07 +05:30
Aaryan KhandelwalandGitHub 7b5ed252ef chore: updated the contact email (#2605)
Updated the contact email to squawk@plane.so
2023-11-02 16:27:23 +05:30
Aaryan KhandelwalandGitHub c394a4f64e style: lite text editor editor toolbar (#2601)
* style: comment editor toolbar

* style: updated icon styling
2023-11-02 16:26:57 +05:30
Dakshesh JainandGitHub 5b808571e5 fix: exception error (#2606)
* fix: exception error

* fix: invitation type
2023-11-02 16:26:16 +05:30
Dakshesh JainandGitHub 2cda47dc8a refactor: user profile store setup and bug fixes (#2586)
* fix: autorun not working when filters are changed

* fix: filter/display on overview page

* refactor: store implementation & loader in 'created' & 'subscribed' page
2023-11-02 16:25:44 +05:30
Anmol Singh BhatiaandGitHub 7f3dbe298c fix: bug fixes (#2607)
* fix: module card issue count fix

* fix: project kanban view add issue bug fix

* fix: draft issue modal button alignment fix
2023-11-02 16:03:03 +05:30
Anmol Singh BhatiaandGitHub 0072160891 fix: peekoverview (#2603)
* fix: peekoverview mutation fix

* fix: peekoverview mutation fix

* fix: sub-issue peekoverview
2023-11-02 16:02:34 +05:30
Anmol Singh BhatiaandGitHub 4512651f8b fix: spreadsheet view properties fix (#2599) 2023-11-02 16:01:49 +05:30
guru_sainathandGitHub f6b95b8d31 fix: Issue properties dropdown overflow issue for date and labels (#2604) 2023-11-02 15:59:43 +05:30
Anmol Singh BhatiaandGitHub 325fb4a377 chore: fixes and improvement (#2595)
* fix: project card fix

* chore: bug fixes and ui improvement
2023-11-02 14:01:56 +05:30
guru_sainathandGitHub ba7b7d6f8b chore: implemented module and cycle select dropdown in issue create modal (#2602) 2023-11-02 13:55:45 +05:30
NikhilandGitHub 7249f84e18 dev: code improvements and minor performance upgrades (#2201)
* dev: remove len for empty comparison

* dev: using in instead of multiple ors

* dev: assign expression to empty variables

* dev: use f-string

* dev: remove list comprehension and use generators

* dev: remove assert from paginator

* dev: use is for identity comparison with singleton

* dev: remove unnecessary else statements

* dev: fix does not exists error for both project and workspace

* dev: remove reimports

* dev: iterate a dictionary

* dev: remove unused commented code

* dev: remove redefinition

* dev: remove unused imports

* dev: remove unused imports

* dev: remove unnecessary f strings

* dev: remove unused variables

* dev: use literal structure to create the data structure

* dev: add empty lines at the end of the file

* dev: remove user middleware

* dev: remove unnecessary default None
2023-11-01 20:35:06 +05:30
d63e7cf254 chore: filters view more and less buttons (#2583)
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2023-11-01 20:34:02 +05:30
Aaryan KhandelwalandGitHub 36152ea2fa chore: loading state for all layouts (#2588)
* chore: add loading states to layouts

* chore: don't show count for 0 inbox issues
2023-11-01 20:24:57 +05:30
Lakhan BahetiandGitHub 1a46c6c399 feat: search project & workspace members (#2590)
* feat: search project & workspace members

* chore: formatting
2023-11-01 20:23:21 +05:30
Bavisetti NarayanandGitHub 4f09a89f5e chore: unarchived issue and date format changes (#2598)
* chore: unarchived issue message corrected

* chore: passing the date in archived at
2023-11-01 20:11:40 +05:30
sriram veeraghantaandGitHub 8c620c4f96 fix: store level fixes (#2597) 2023-11-01 19:22:10 +05:30
Anmol Singh BhatiaandGitHub d46eb9c59a chore: add issue option added in header group (#2592)
* chore: add issue option added in list view group header

* chore: add issue option added in kanban view group header
2023-11-01 19:10:29 +05:30
Bavisetti NarayanandGitHub e9321a66e7 chore: added validation for archived issue (#2593)
* chore: added validation for archived issue

* fix: optimised code
2023-11-01 17:20:55 +05:30
sriram veeraghantaandGitHub 0121a4ab51 [FED-594] fix: user change theme interface bug fixes (#2587)
* fix: user change theme interface bugfixes

* fix: handling error case
2023-11-01 17:11:29 +05:30
Anmol Singh BhatiaandGitHub 548e95c7e0 fix: bug fixes (#2581)
* fix: module sidebar fix for kanban layout

* chore: cycle & module sidebar improvement

* chore: join project content updated

* chore: project empty state header fix

* chore: create project modal dropdown consistency

* chore: list view group header overlapping issue fix

* chore: popover code refactor

* chore: module sidebar fix for cycle kanban view

* chore: add existing issue option added in module empty state

* chore: add existing issue option added in cycle empty state
2023-11-01 17:11:07 +05:30
Aaryan KhandelwalandGitHub 13ead7c314 fix: project wrapper (#2589)
* fix: project wrapper

* fix: project wrapper for unjoined project

* chore: update store structure
2023-11-01 17:10:10 +05:30
sriram veeraghantaandGitHub 4fcc4b4a01 fix: build fixes (#2591) 2023-11-01 16:56:44 +05:30
d511799f31 [FEATURE] Enabled User @mentions and @mention-filters in core editor package (#2544)
* feat: created custom mention component

* feat: added mention suggestions and suggestion highlights

* feat: created mention suggestion list for displaying mention suggestions

* feat: created custom mention text component, for handling click event

* feat: exposed mention component

* feat: integrated and exposed `mentions` componenet with `editor-core`

* feat: integrated mentions extension with the core editor package

* feat: exposed suggestion types from mentions

* feat: added `mention-suggestion` parameters in `r-t-e` and `l-t-e`

* feat: added `IssueMention` model in apiserver models

* chore: updated activities background job and added bs4 in requirements

* feat: added mention removal logic in issue_activity

* chore: exposed mention types from `r-t-e` and `l-t-e`

* feat: integrated mentions in side peek view description form

* feat: added mentions in issue modal form

* feat: created custom react-hook for editor suggestions

* feat: integrated mention suggestions block in RichTextEditor

* feat: added `mentions` integration in `lite-text-editor` instances

* fix: tailwind loading nodemodules from packages

* feat: added styles for the mention suggestion list

* fix: update module import to resolve build failure

* feat: added mentions as an issue filter

* feat: added UI Changes to Implement `mention` filters

* feat: added `mentions` as a filter option in the header

* feat: added mentions in the filter list options

* feat: added mentions in default display filter options

* feat: added filters in applied and issue params in store

* feat: modified types for adding mentions as a filter option

* feat: modified `notification-card` to display message when it exists in object

* feat: rewrote user mention management upon the changes made in develop

* chore: merged debounce PR with the current PR for tracing changes

* fix: mentions_filters updated with the new setup

* feat: updated requirements for bs4

* feat: modified `mentions-filter` to remove many to many dependency

* feat: implemented list manipulation instead of for loop

* feat: added readonly functionality in `read-only` editor core

* feat: added UI Changes for read-only mode

* feat: added mentions store in web Root Store

* chore: renamed `use-editor-suggestions` hook

* feat: UI Improvements for conditional highlights w.r.t readonly in mentionNode

* fix: removed mentions from `filter_set` parameters

* fix: minor merge fixes

* fix: package lock updates

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2023-11-01 16:36:37 +05:30
Aaryan KhandelwalandGitHub 490e032ac6 style: new avatar and avatar group components (#2584)
* style: new avatar components

* chore: bug fixes

* chore: add pixel to size

* chore: add comments to helper functions

* fix: build errors
2023-11-01 15:24:11 +05:30
Bavisetti NarayanandGitHub 1a24f9ec25 chore: removed start date and target date filter (#2582) 2023-11-01 14:34:03 +05:30
Bavisetti NarayanandGitHub 2cb94b4105 chore: added permission for importer (#2577) 2023-11-01 14:32:33 +05:30
Bavisetti NarayanandGitHub ecde7edf09 fix: removed numbers from magic code (#2570) 2023-11-01 14:31:42 +05:30
Bavisetti NarayanandGitHub 02f4916e49 chore: workspace members and project members endpoint (#2560)
* fix: removed members endpoint

* fix: changed project permisson class for project member

* fix: permission changed in workspace and project

* fix: added project filter in members
2023-11-01 14:30:45 +05:30
1be82814fc style: issue peek overview ui improvement (#2574)
* style: issue peek overview ui improvement

* chore: implemented issue subscription in peek overview

* chore: issue properties dropdown refactor

* fix: build error

* chore: label select refactor

* chore: issue peekoverview revamp and refactor

* chore: issue peekoverview properties added and code refactor

---------

Co-authored-by: gurusainath <gurusainath007@gmail.com>
2023-11-01 14:22:29 +05:30
sriram veeraghantaandGitHub 10e35d9a06 chore: delete unused files (#2585) 2023-11-01 13:45:04 +05:30
Dakshesh JainandGitHub 2d64caef90 refactor: project settings (#2575)
* refactor: project setting estimate

* refactor: project setting label

* refactor: project setting state

* refactor: project setting integration

* refactor: project settings member

* fix: estimate not updating

* fix: estimate not in observable

* fix: build error
2023-11-01 13:42:51 +05:30
deepsource-autofix[bot]GitHubdeepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com>
80e6d7e1ea refactor: remove true from boolean attribute (#2579)
When using a boolean attribute in JSX, you can set the attribute value to true or omit the value. This helps to keep consistency in code.

Co-authored-by: deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com>
2023-11-01 12:30:21 +05:30
sriram veeraghantaandGitHub b7d5a42d45 fix: added deepsource config file (#2578) 2023-10-31 19:52:13 +05:30
sriram veeraghantaandGitHub 2b1e1557ca fix: upgrading to turbo new version (#2576) 2023-10-31 19:27:56 +05:30
Aaryan KhandelwalandGitHub 705b33377c fix: members list endpoint authorization (#2571)
* fix: members list endpoint authorization

* chore: update user types
2023-10-31 13:11:13 +05:30
NikhilandGitHub 49fd4427c8 chore: user settings endpoint (#2557)
* chore: user settings endpoint

* dev: fix the user settings
2023-10-31 13:08:13 +05:30
Bavisetti NarayanandGitHub bdbb64f385 fix: changed assignees and labels in pages and modules (#2553) 2023-10-31 13:06:57 +05:30
Aaryan KhandelwalandGitHub 98716859d5 chore: reove unused files (#2567) 2023-10-31 12:43:08 +05:30
M. PalanikannanandGitHub 8072bbb559 fix: Debounce title and Editor initialization (#2530)
* fixed debounce logic and extracted the same

* fixed editor mounting with custom hook

* removed console logs and improved structure

* fixed comment editor behavior on Shift-Enter

* fixed editor initialization behaviour for new peek view

* fixed button type to avoid reload while editing comments

* fixed initialization of content in peek overview

* improved naming variables in updated title debounce logic

* added react-hook-form support to the issue detail in peek view with save states

* delete image plugin's ts support improved
2023-10-31 12:26:10 +05:30
Aaryan KhandelwalandGitHub 442c83eea2 style: spreadsheet columns (#2554)
* style: spreadsheet columns

* fix: build errors
2023-10-31 12:18:04 +05:30
Aaryan KhandelwalandGitHub cb533849e8 chore: update members endpoint (#2569) 2023-10-31 12:16:40 +05:30
Aaryan KhandelwalandGitHub 59c52023fb style: list layout (#2566) 2023-10-31 12:14:06 +05:30
Aaryan KhandelwalandGitHub 08ca016f65 fix: custom theme form validations (#2565) 2023-10-31 12:12:24 +05:30
Aaryan KhandelwalandGitHub 1c2ea6da5e fix: edit project button redirection (#2564)
* fix: redirect to project settings

* fix: 404 page button alignment
2023-10-31 12:06:55 +05:30
Aaryan KhandelwalandGitHub 8b7b5c54b9 fix: global views bugs (#2563) 2023-10-31 12:06:11 +05:30
guru_sainathandGitHub 52474715de chore: handled next_url redirection issue (#2562) 2023-10-31 12:04:36 +05:30
Aaryan KhandelwalandGitHub dcf81e28e4 dev: implemented MobX in workspace settings and create workspace form (#2561)
* dev: implement mobx store for workspace settings

* chore: workspace general settings mobx integration

* chore: workspace members settings mobx integration
2023-10-30 20:38:50 +05:30
Aaryan KhandelwalandGitHub 050406b8a4 chore: add empty state for list and spreadsheet layouts (#2531)
* chore: add empty state for list and spreadsheet layouts

* fix: build errors
2023-10-30 20:09:04 +05:30
Anmol Singh BhatiaandGitHub 8eaac60aa5 style: cycle ui revamp, and chore: code refactor (#2558)
* chore: cycle custom svg icon added and code refactor

* chore: module code refactor

* style: cycle ui revamp and code refactor

* chore: cycle card view layout fix

* chore: layout fix

* style: module and cycle title tooltip position
2023-10-30 19:22:27 +05:30
1145 changed files with 36625 additions and 32281 deletions
+17
View File
@@ -0,0 +1,17 @@
version = 1
[[analyzers]]
name = "shell"
[[analyzers]]
name = "javascript"
[analyzers.meta]
plugins = ["react"]
environment = ["nodejs"]
[[analyzers]]
name = "python"
[analyzers.meta]
runtime_version = "3.x.x"
+5 -5
View File
@@ -21,15 +21,15 @@ AWS_S3_BUCKET_NAME="uploads"
FILE_SIZE_LIMIT=5242880
# GPT settings
OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint
OPENAI_API_KEY="sk-" # add your openai key here
GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access
OPENAI_API_BASE="https://api.openai.com/v1" # deprecated
OPENAI_API_KEY="sk-" # deprecated
GPT_ENGINE="gpt-3.5-turbo" # deprecated
# Settings related to Docker
DOCKERIZED=1
DOCKERIZED=1 # deprecated
# set to 1 If using the pre-configured minio setup
USE_MINIO=1
# Nginx Configuration
NGINX_PORT=80
+213
View File
@@ -0,0 +1,213 @@
name: Branch Build
on:
pull_request:
types:
- closed
branches:
- master
- release
- qa
- develop
env:
TARGET_BRANCH: ${{ github.event.pull_request.base.ref }}
jobs:
branch_build_and_push:
if: ${{ (github.event_name == 'pull_request' && github.event.action =='closed' && github.event.pull_request.merged == true) }}
name: Build-Push Web/Space/API/Proxy Docker Image
runs-on: ubuntu-20.04
steps:
- name: Check out the repo
uses: actions/checkout@v3.3.0
# - name: Set Target Branch Name on PR close
# if: ${{ github.event_name == 'pull_request' && github.event.action =='closed' }}
# run: echo "TARGET_BRANCH=${{ github.event.pull_request.base.ref }}" >> $GITHUB_ENV
# - name: Set Target Branch Name on other than PR close
# if: ${{ github.event_name == 'push' }}
# run: echo "TARGET_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV
- uses: ASzc/change-string-case-action@v2
id: gh_branch_upper_lower
with:
string: ${{env.TARGET_BRANCH}}
- uses: mad9000/actions-find-and-replace-string@2
id: gh_branch_replace_slash
with:
source: ${{ steps.gh_branch_upper_lower.outputs.lowercase }}
find: '/'
replace: '-'
- uses: mad9000/actions-find-and-replace-string@2
id: gh_branch_replace_dot
with:
source: ${{ steps.gh_branch_replace_slash.outputs.value }}
find: '.'
replace: ''
- uses: mad9000/actions-find-and-replace-string@2
id: gh_branch_clean
with:
source: ${{ steps.gh_branch_replace_dot.outputs.value }}
find: '_'
replace: ''
- name: Uploading Proxy Source
uses: actions/upload-artifact@v3
with:
name: proxy-src-code
path: ./nginx
- name: Uploading Backend Source
uses: actions/upload-artifact@v3
with:
name: backend-src-code
path: ./apiserver
- name: Uploading Web Source
uses: actions/upload-artifact@v3
with:
name: web-src-code
path: |
./
!./apiserver
!./nginx
!./deploy
!./space
- name: Uploading Space Source
uses: actions/upload-artifact@v3
with:
name: space-src-code
path: |
./
!./apiserver
!./nginx
!./deploy
!./web
outputs:
gh_branch_name: ${{ steps.gh_branch_clean.outputs.value }}
branch_build_push_frontend:
runs-on: ubuntu-20.04
needs: [ branch_build_and_push ]
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.5.0
- name: Login to Docker Hub
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Downloading Web Source Code
uses: actions/download-artifact@v3
with:
name: web-src-code
- name: Build and Push Frontend to Docker Container Registry
uses: docker/build-push-action@v4.0.0
with:
context: .
file: ./web/Dockerfile.web
platforms: linux/amd64
tags: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend-private:${{ needs.branch_build_and_push.outputs.gh_branch_name }}
push: true
env:
DOCKER_BUILDKIT: 1
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
branch_build_push_space:
runs-on: ubuntu-20.04
needs: [ branch_build_and_push ]
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.5.0
- name: Login to Docker Hub
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Downloading Space Source Code
uses: actions/download-artifact@v3
with:
name: space-src-code
- name: Build and Push Space to Docker Hub
uses: docker/build-push-action@v4.0.0
with:
context: .
file: ./space/Dockerfile.space
platforms: linux/amd64
tags: ${{ secrets.DOCKERHUB_USERNAME }}/plane-space-private:${{ needs.branch_build_and_push.outputs.gh_branch_name }}
push: true
env:
DOCKER_BUILDKIT: 1
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
branch_build_push_backend:
runs-on: ubuntu-20.04
needs: [ branch_build_and_push ]
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.5.0
- name: Login to Docker Hub
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Downloading Backend Source Code
uses: actions/download-artifact@v3
with:
name: backend-src-code
- name: Build and Push Backend to Docker Hub
uses: docker/build-push-action@v4.0.0
with:
context: .
file: ./Dockerfile.api
platforms: linux/amd64
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend-private:${{ needs.branch_build_and_push.outputs.gh_branch_name }}
env:
DOCKER_BUILDKIT: 1
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
branch_build_push_proxy:
runs-on: ubuntu-20.04
needs: [ branch_build_and_push ]
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.5.0
- name: Login to Docker Hub
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Downloading Proxy Source Code
uses: actions/download-artifact@v3
with:
name: proxy-src-code
- name: Build and Push Plane-Proxy to Docker Hub
uses: docker/build-push-action@v4.0.0
with:
context: .
file: ./Dockerfile
platforms: linux/amd64
tags: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy-private:${{ needs.branch_build_and_push.outputs.gh_branch_name }}
push: true
env:
DOCKER_BUILDKIT: 1
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
+2 -1
View File
@@ -75,7 +75,8 @@ pnpm-lock.yaml
pnpm-workspace.yaml
.npmrc
.secrets
tmp/
## packages
dist
.temp/
+1 -1
View File
@@ -60,7 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
hello@plane.so.
squawk@plane.so.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
+16 -16
View File
@@ -8,8 +8,8 @@ Before submitting a new issue, please search the [issues](https://github.com/mak
While we want to fix all the [issues](https://github.com/makeplane/plane/issues), before fixing a bug we need to be able to reproduce and confirm it. Please provide us with a minimal reproduction scenario using a repository or [Gist](https://gist.github.com/). Having a live, reproducible scenario gives us the information without asking questions back & forth with additional questions like:
- 3rd-party libraries being used and their versions
- a use-case that fails
- 3rd-party libraries being used and their versions
- a use-case that fails
Without said minimal reproduction, we won't be able to investigate all [issues](https://github.com/makeplane/plane/issues), and the issue might not be resolved.
@@ -19,10 +19,10 @@ You can open a new issue with this [issue form](https://github.com/makeplane/pla
### Requirements
- Node.js version v16.18.0
- Python version 3.8+
- Postgres version v14
- Redis version v6.2.7
- Node.js version v16.18.0
- Python version 3.8+
- Postgres version v14
- Redis version v6.2.7
### Setup the project
@@ -81,8 +81,8 @@ If you would like to _implement_ it, an issue with your proposal must be submitt
To ensure consistency throughout the source code, please keep these rules in mind as you are working:
- All features or bug fixes must be tested by one or more specs (unit-tests).
- We use [Eslint default rule guide](https://eslint.org/docs/rules/), with minor changes. An automated formatter is available using prettier.
- All features or bug fixes must be tested by one or more specs (unit-tests).
- We use [Eslint default rule guide](https://eslint.org/docs/rules/), with minor changes. An automated formatter is available using prettier.
## Need help? Questions and suggestions
@@ -90,11 +90,11 @@ Questions, suggestions, and thoughts are most welcome. We can also be reached in
## Ways to contribute
- Try Plane Cloud and the self hosting platform and give feedback
- Add new integrations
- Help with open [issues](https://github.com/makeplane/plane/issues) or [create your own](https://github.com/makeplane/plane/issues/new/choose)
- Share your thoughts and suggestions with us
- Help create tutorials and blog posts
- Request a feature by submitting a proposal
- Report a bug
- **Improve documentation** - fix incomplete or missing [docs](https://docs.plane.so/), bad wording, examples or explanations.
- Try Plane Cloud and the self hosting platform and give feedback
- Add new integrations
- Help with open [issues](https://github.com/makeplane/plane/issues) or [create your own](https://github.com/makeplane/plane/issues/new/choose)
- Share your thoughts and suggestions with us
- Help create tutorials and blog posts
- Request a feature by submitting a proposal
- Report a bug
- **Improve documentation** - fix incomplete or missing [docs](https://docs.plane.so/), bad wording, examples or explanations.
-2
View File
@@ -43,8 +43,6 @@ FROM python:3.11.1-alpine3.17 AS backend
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
ENV DJANGO_SETTINGS_MODULE plane.settings.production
ENV DOCKERIZED 1
WORKDIR /code
+27 -15
View File
@@ -1,8 +1,10 @@
# Environment Variables
Environment variables are distributed in various files. Please refer them carefully.
Environment variables are distributed in various files. Please refer them carefully.
## {PROJECT_FOLDER}/.env
File is available in the project root folder
```
@@ -29,42 +31,52 @@ AWS_S3_BUCKET_NAME="uploads"
FILE_SIZE_LIMIT=5242880
# GPT settings
OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint
OPENAI_API_KEY="sk-" # add your openai key here
GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access
OPENAI_API_BASE="https://api.openai.com/v1" # deprecated
OPENAI_API_KEY="sk-" # deprecated
GPT_ENGINE="gpt-3.5-turbo" # deprecated
# Settings related to Docker
DOCKERIZED=1
# set to 1 If using the pre-configured minio setup
USE_MINIO=1
# Nginx Configuration
NGINX_PORT=80
```
## {PROJECT_FOLDER}/web/.env.example
```
# Enable/Disable OAUTH - default 0 for selfhosted instance
NEXT_PUBLIC_ENABLE_OAUTH=0
# Public boards deploy URL
NEXT_PUBLIC_DEPLOY_URL="http://localhost/spaces"
```
## {PROJECT_FOLDER}/spaces/.env.example
```
# Flag to toggle OAuth
NEXT_PUBLIC_ENABLE_OAUTH=0
```
## {PROJECT_FOLDER}/apiserver/.env
```
# Backend
# Debug value for api server use it as 0 for production use
DEBUG=0
DJANGO_SETTINGS_MODULE="plane.settings.selfhosted"
DJANGO_SETTINGS_MODULE="plane.settings.selfhosted" # deprecated
# Error logs
SENTRY_DSN=""
@@ -101,24 +113,22 @@ AWS_S3_BUCKET_NAME="uploads"
FILE_SIZE_LIMIT=5242880
# GPT settings
OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint
OPENAI_API_KEY="sk-" # add your openai key here
GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access
OPENAI_API_BASE="https://api.openai.com/v1" # deprecated
OPENAI_API_KEY="sk-" # deprecated
GPT_ENGINE="gpt-3.5-turbo" # deprecated
# Settings related to Docker
DOCKERIZED=1 # Deprecated
# Github
GITHUB_CLIENT_SECRET="" # For fetching release notes
# Settings related to Docker
DOCKERIZED=1
# set to 1 If using the pre-configured minio setup
USE_MINIO=1
# Nginx Configuration
NGINX_PORT=80
# Default Creds
DEFAULT_EMAIL="captain@plane.so"
DEFAULT_PASSWORD="password123"
# SignUps
ENABLE_SIGNUP="1"
@@ -126,7 +136,9 @@ ENABLE_SIGNUP="1"
# Email Redirection URL
WEB_URL="http://localhost"
```
## Updates
- The environment variable NEXT_PUBLIC_API_BASE_URL has been removed from both the web and space projects.
- The naming convention for containers and images has been updated.
- The plane-worker image will no longer be maintained, as it has been merged with plane-backend.
+1 -1
View File
@@ -7,7 +7,7 @@
</p>
<h3 align="center"><b>Plane</b></h3>
<p align="center"><b>Open-source, self-hosted project planning tool</b></p>
<p align="center"><b>Flexible, extensible open-source project management</b></p>
<p align="center">
<a href="https://discord.com/invite/A92xrEGCge">
+8 -16
View File
@@ -1,10 +1,11 @@
# Backend
# Debug value for api server use it as 0 for production use
DEBUG=0
DJANGO_SETTINGS_MODULE="plane.settings.production"
CORS_ALLOWED_ORIGINS=""
# Error logs
SENTRY_DSN=""
SENTRY_ENVIRONMENT="development"
# Database Settings
PGUSER="plane"
@@ -18,15 +19,6 @@ REDIS_HOST="plane-redis"
REDIS_PORT="6379"
REDIS_URL="redis://${REDIS_HOST}:6379/"
# Email Settings
EMAIL_HOST=""
EMAIL_HOST_USER=""
EMAIL_HOST_PASSWORD=""
EMAIL_PORT=587
EMAIL_FROM="Team Plane <team@mailer.plane.so>"
EMAIL_USE_TLS="1"
EMAIL_USE_SSL="0"
# AWS Settings
AWS_REGION=""
AWS_ACCESS_KEY_ID="access-key"
@@ -38,9 +30,9 @@ AWS_S3_BUCKET_NAME="uploads"
FILE_SIZE_LIMIT=5242880
# GPT settings
OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint
OPENAI_API_KEY="sk-" # add your openai key here
GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access
OPENAI_API_BASE="https://api.openai.com/v1" # deprecated
OPENAI_API_KEY="sk-" # deprecated
GPT_ENGINE="gpt-3.5-turbo" # deprecated
# Github
GITHUB_CLIENT_SECRET="" # For fetching release notes
@@ -53,9 +45,6 @@ USE_MINIO=1
# Nginx Configuration
NGINX_PORT=80
# Default Creds
DEFAULT_EMAIL="captain@plane.so"
DEFAULT_PASSWORD="password123"
# SignUps
ENABLE_SIGNUP="1"
@@ -70,3 +59,6 @@ ENABLE_MAGIC_LINK_LOGIN="0"
# Email redirections and minio domain settings
WEB_URL="http://localhost"
# Gunicorn Workers
GUNICORN_WORKERS=2
+1 -1
View File
@@ -43,7 +43,7 @@ USER captain
COPY manage.py manage.py
COPY plane plane/
COPY templates templates/
COPY package.json package.json
COPY gunicorn.config.py ./
USER root
RUN apk --no-cache add "bash~=5.2"
+14 -3
View File
@@ -3,7 +3,18 @@ set -e
python manage.py wait_for_db
python manage.py migrate
# Create a Default User
python bin/user_script.py
# Set default value for ENABLE_REGISTRATION
ENABLE_REGISTRATION=${ENABLE_REGISTRATION:-1}
exec gunicorn -w 8 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile -
# Check if ENABLE_REGISTRATION is not set to '0'
if [ "$ENABLE_REGISTRATION" != "0" ]; then
# Register instance
python manage.py register_instance
# Load the configuration variable
python manage.py configure_instance
fi
# Create the default bucket
python bin/bucket_script.py
exec gunicorn -w $GUNICORN_WORKERS -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile -
-28
View File
@@ -1,28 +0,0 @@
import os, sys, random, string
import uuid
sys.path.append("/code")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production")
import django
django.setup()
from plane.db.models import User
def populate():
default_email = os.environ.get("DEFAULT_EMAIL", "captain@plane.so")
default_password = os.environ.get("DEFAULT_PASSWORD", "password123")
if not User.objects.filter(email=default_email).exists():
user = User.objects.create(email=default_email, username=uuid.uuid4().hex)
user.set_password(default_password)
user.save()
print(f"User created with an email: {default_email}")
else:
print(f"User already exists with the default email: {default_email}")
if __name__ == "__main__":
populate()
+1 -1
View File
@@ -3,4 +3,4 @@ from psycogreen.gevent import patch_psycopg
def post_fork(server, worker):
patch_psycopg()
worker.log.info("Made Psycopg2 Green")
worker.log.info("Made Psycopg2 Green")
+4
View File
@@ -0,0 +1,4 @@
{
"name": "plane-api",
"version": "0.13.2"
}
+8
View File
@@ -0,0 +1,8 @@
# from django.contrib import admin
from django.urls import path
from plane.analytics.views import EventTrackingEndpoint
urlpatterns = [
path('track-event/', EventTrackingEndpoint.as_view(), name='event_tracking'),
]
@@ -0,0 +1 @@
from .event_tacking import EventTrackingEndpoint
@@ -0,0 +1,52 @@
# Python imports
import requests
import json
# Module imports
from plane.app.views import BaseAPIView
# Third party imports
from rest_framework import status
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
# Django imports
from django.conf import settings
from django.core.serializers.json import DjangoJSONEncoder
class EventTrackingEndpoint(BaseAPIView):
permission_class = [
IsAuthenticated,
]
def post(self, request):
event_name = request.data.get("event_name", None)
extra = request.data.get("extra", None)
if not event_name or not extra:
return Response(
{"error": "event name and extra are required"},
status=status.HTTP_400_BAD_REQUEST,
)
_ = requests.post(
settings.ANALYTICS_BASE_API,
params={"token": settings.ANALYTICS_SECRET_KEY},
json=json.dumps(
{
"eventName": event_name,
"extra": extra,
"user": {
"id": str(request.user.id),
"email": str(request.user.email),
"first_name": str(request.user.first_name),
"last_name": str(request.user.last_name),
"last_login_ip": str(request.META.get("REMOTE_ADDR")),
"last_login_uagent": str(request.META.get("HTTP_USER_AGENT"))
},
},
cls=DjangoJSONEncoder,
),
)
return Response({"message": "success"}, status=status.HTTP_200_OK)
@@ -1,2 +0,0 @@
from .workspace import WorkSpaceBasePermission, WorkSpaceAdminPermission, WorkspaceEntityPermission, WorkspaceViewerPermission
from .project import ProjectBasePermission, ProjectEntityPermission, ProjectMemberPermission, ProjectLitePermission
@@ -1,14 +0,0 @@
from .base import BaseSerializer
from plane.db.models import APIToken
class APITokenSerializer(BaseSerializer):
class Meta:
model = APIToken
fields = [
"label",
"user",
"user_type",
"workspace",
"created_at",
]
-13
View File
@@ -1,13 +0,0 @@
from django.urls import path
from plane.api.views import GPTIntegrationEndpoint
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/ai-assistant/",
GPTIntegrationEndpoint.as_view(),
name="importer",
),
]
-79
View File
@@ -1,79 +0,0 @@
from django.urls import path
from plane.api.views import (
PageViewSet,
PageBlockViewSet,
PageFavoriteViewSet,
CreateIssueFromPageBlockEndpoint,
)
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/",
PageViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/",
PageViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/page-blocks/",
PageBlockViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-page-blocks",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/page-blocks/<uuid:pk>/",
PageBlockViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-page-blocks",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-pages/",
PageFavoriteViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="user-favorite-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-pages/<uuid:page_id>/",
PageFavoriteViewSet.as_view(
{
"delete": "destroy",
}
),
name="user-favorite-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/page-blocks/<uuid:page_block_id>/issues/",
CreateIssueFromPageBlockEndpoint.as_view(),
name="page-block-issues",
),
]
-151
View File
@@ -1,151 +0,0 @@
from django.urls import path
from plane.api.views import (
ProjectDeployBoardViewSet,
ProjectDeployBoardPublicSettingsEndpoint,
ProjectIssuesPublicEndpoint,
IssueRetrievePublicEndpoint,
IssueCommentPublicViewSet,
IssueReactionPublicViewSet,
CommentReactionPublicViewSet,
InboxIssuePublicViewSet,
IssueVotePublicViewSet,
WorkspaceProjectDeployBoardEndpoint,
)
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/",
ProjectDeployBoardViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-deploy-board",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/<uuid:pk>/",
ProjectDeployBoardViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-deploy-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/settings/",
ProjectDeployBoardPublicSettingsEndpoint.as_view(),
name="project-deploy-board-settings",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/",
ProjectIssuesPublicEndpoint.as_view(),
name="project-deploy-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/",
IssueRetrievePublicEndpoint.as_view(),
name="workspace-project-boards",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/comments/",
IssueCommentPublicViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="issue-comments-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/comments/<uuid:pk>/",
IssueCommentPublicViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="issue-comments-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/reactions/",
IssueReactionPublicViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="issue-reactions-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/reactions/<str:reaction_code>/",
IssueReactionPublicViewSet.as_view(
{
"delete": "destroy",
}
),
name="issue-reactions-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/comments/<uuid:comment_id>/reactions/",
CommentReactionPublicViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="comment-reactions-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/comments/<uuid:comment_id>/reactions/<str:reaction_code>/",
CommentReactionPublicViewSet.as_view(
{
"delete": "destroy",
}
),
name="comment-reactions-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/",
InboxIssuePublicViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="inbox-issue",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:pk>/",
InboxIssuePublicViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="inbox-issue",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/votes/",
IssueVotePublicViewSet.as_view(
{
"get": "list",
"post": "create",
"delete": "destroy",
}
),
name="issue-vote-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/",
WorkspaceProjectDeployBoardEndpoint.as_view(),
name="workspace-project-boards",
),
]
-13
View File
@@ -1,13 +0,0 @@
from django.urls import path
from plane.api.views import ReleaseNotesEndpoint
urlpatterns = [
path(
"release-notes/",
ReleaseNotesEndpoint.as_view(),
name="release-notes",
),
]
-13
View File
@@ -1,13 +0,0 @@
from django.urls import path
from plane.api.views import UnsplashEndpoint
urlpatterns = [
path(
"unsplash/",
UnsplashEndpoint.as_view(),
name="unsplash",
),
]
-47
View File
@@ -1,47 +0,0 @@
# Python import
from uuid import uuid4
# Third party
from rest_framework.response import Response
from rest_framework import status
from sentry_sdk import capture_exception
# Module import
from .base import BaseAPIView
from plane.db.models import APIToken
from plane.api.serializers import APITokenSerializer
class ApiTokenEndpoint(BaseAPIView):
def post(self, request):
label = request.data.get("label", str(uuid4().hex))
workspace = request.data.get("workspace", False)
if not workspace:
return Response(
{"error": "Workspace is required"}, status=status.HTTP_200_OK
)
api_token = APIToken.objects.create(
label=label, user=request.user, workspace_id=workspace
)
serializer = APITokenSerializer(api_token)
# Token will be only vissible while creating
return Response(
{"api_token": serializer.data, "token": api_token.token},
status=status.HTTP_201_CREATED,
)
def get(self, request):
api_tokens = APIToken.objects.filter(user=request.user)
serializer = APITokenSerializer(api_tokens, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
def delete(self, request, pk):
api_token = APIToken.objects.get(pk=pk)
api_token.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
-397
View File
@@ -1,397 +0,0 @@
# Python imports
import uuid
import random
import string
import json
import requests
# Django imports
from django.utils import timezone
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
from django.conf import settings
from django.contrib.auth.hashers import make_password
# Third party imports
from rest_framework.response import Response
from rest_framework.permissions import AllowAny
from rest_framework import status
from rest_framework_simplejwt.tokens import RefreshToken
from sentry_sdk import capture_exception, capture_message
# Module imports
from . import BaseAPIView
from plane.db.models import User
from plane.api.serializers import UserSerializer
from plane.settings.redis import redis_instance
from plane.bgtasks.magic_link_code_task import magic_link
def get_tokens_for_user(user):
refresh = RefreshToken.for_user(user)
return (
str(refresh.access_token),
str(refresh),
)
class SignUpEndpoint(BaseAPIView):
permission_classes = (AllowAny,)
def post(self, request):
if not settings.ENABLE_SIGNUP:
return Response(
{
"error": "New account creation is disabled. Please contact your site administrator"
},
status=status.HTTP_400_BAD_REQUEST,
)
email = request.data.get("email", False)
password = request.data.get("password", False)
## Raise exception if any of the above are missing
if not email or not password:
return Response(
{"error": "Both email and password are required"},
status=status.HTTP_400_BAD_REQUEST,
)
email = email.strip().lower()
try:
validate_email(email)
except ValidationError as e:
return Response(
{"error": "Please provide a valid email address."},
status=status.HTTP_400_BAD_REQUEST,
)
# Check if the user already exists
if User.objects.filter(email=email).exists():
return Response(
{"error": "User with this email already exists"},
status=status.HTTP_400_BAD_REQUEST,
)
user = User.objects.create(email=email, username=uuid.uuid4().hex)
user.set_password(password)
# settings last actives for the user
user.last_active = timezone.now()
user.last_login_time = timezone.now()
user.last_login_ip = request.META.get("REMOTE_ADDR")
user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
user.token_updated_at = timezone.now()
user.save()
access_token, refresh_token = get_tokens_for_user(user)
data = {
"access_token": access_token,
"refresh_token": refresh_token,
}
# Send Analytics
if settings.ANALYTICS_BASE_API:
_ = requests.post(
settings.ANALYTICS_BASE_API,
headers={
"Content-Type": "application/json",
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
},
json={
"event_id": uuid.uuid4().hex,
"event_data": {
"medium": "email",
},
"user": {"email": email, "id": str(user.id)},
"device_ctx": {
"ip": request.META.get("REMOTE_ADDR"),
"user_agent": request.META.get("HTTP_USER_AGENT"),
},
"event_type": "SIGN_UP",
},
)
return Response(data, status=status.HTTP_200_OK)
class SignInEndpoint(BaseAPIView):
permission_classes = (AllowAny,)
def post(self, request):
email = request.data.get("email", False)
password = request.data.get("password", False)
## Raise exception if any of the above are missing
if not email or not password:
return Response(
{"error": "Both email and password are required"},
status=status.HTTP_400_BAD_REQUEST,
)
email = email.strip().lower()
try:
validate_email(email)
except ValidationError as e:
return Response(
{"error": "Please provide a valid email address."},
status=status.HTTP_400_BAD_REQUEST,
)
user = User.objects.filter(email=email).first()
if user is None:
return Response(
{
"error": "Sorry, we could not find a user with the provided credentials. Please try again."
},
status=status.HTTP_403_FORBIDDEN,
)
# Sign up Process
if not user.check_password(password):
return Response(
{
"error": "Sorry, we could not find a user with the provided credentials. Please try again."
},
status=status.HTTP_403_FORBIDDEN,
)
if not user.is_active:
return Response(
{
"error": "Your account has been deactivated. Please contact your site administrator."
},
status=status.HTTP_403_FORBIDDEN,
)
# settings last active for the user
user.last_active = timezone.now()
user.last_login_time = timezone.now()
user.last_login_ip = request.META.get("REMOTE_ADDR")
user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
user.token_updated_at = timezone.now()
user.save()
access_token, refresh_token = get_tokens_for_user(user)
# Send Analytics
if settings.ANALYTICS_BASE_API:
_ = requests.post(
settings.ANALYTICS_BASE_API,
headers={
"Content-Type": "application/json",
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
},
json={
"event_id": uuid.uuid4().hex,
"event_data": {
"medium": "email",
},
"user": {"email": email, "id": str(user.id)},
"device_ctx": {
"ip": request.META.get("REMOTE_ADDR"),
"user_agent": request.META.get("HTTP_USER_AGENT"),
},
"event_type": "SIGN_IN",
},
)
data = {
"access_token": access_token,
"refresh_token": refresh_token,
}
return Response(data, status=status.HTTP_200_OK)
class SignOutEndpoint(BaseAPIView):
def post(self, request):
refresh_token = request.data.get("refresh_token", False)
if not refresh_token:
capture_message("No refresh token provided")
return Response(
{"error": "No refresh token provided"},
status=status.HTTP_400_BAD_REQUEST,
)
user = User.objects.get(pk=request.user.id)
user.last_logout_time = timezone.now()
user.last_logout_ip = request.META.get("REMOTE_ADDR")
user.save()
token = RefreshToken(refresh_token)
token.blacklist()
return Response({"message": "success"}, status=status.HTTP_200_OK)
class MagicSignInGenerateEndpoint(BaseAPIView):
permission_classes = [
AllowAny,
]
def post(self, request):
email = request.data.get("email", False)
if not email:
return Response(
{"error": "Please provide a valid email address"},
status=status.HTTP_400_BAD_REQUEST,
)
# Clean up
email = email.strip().lower()
validate_email(email)
## Generate a random token
token = (
"".join(random.choices(string.ascii_lowercase + string.digits, k=4))
+ "-"
+ "".join(random.choices(string.ascii_lowercase + string.digits, k=4))
+ "-"
+ "".join(random.choices(string.ascii_lowercase + string.digits, k=4))
)
ri = redis_instance()
key = "magic_" + str(email)
# Check if the key already exists in python
if ri.exists(key):
data = json.loads(ri.get(key))
current_attempt = data["current_attempt"] + 1
if data["current_attempt"] > 2:
return Response(
{"error": "Max attempts exhausted. Please try again later."},
status=status.HTTP_400_BAD_REQUEST,
)
value = {
"current_attempt": current_attempt,
"email": email,
"token": token,
}
expiry = 600
ri.set(key, json.dumps(value), ex=expiry)
else:
value = {"current_attempt": 0, "email": email, "token": token}
expiry = 600
ri.set(key, json.dumps(value), ex=expiry)
current_site = settings.WEB_URL
magic_link.delay(email, key, token, current_site)
return Response({"key": key}, status=status.HTTP_200_OK)
class MagicSignInEndpoint(BaseAPIView):
permission_classes = [
AllowAny,
]
def post(self, request):
user_token = request.data.get("token", "").strip()
key = request.data.get("key", False).strip().lower()
if not key or user_token == "":
return Response(
{"error": "User token and key are required"},
status=status.HTTP_400_BAD_REQUEST,
)
ri = redis_instance()
if ri.exists(key):
data = json.loads(ri.get(key))
token = data["token"]
email = data["email"]
if str(token) == str(user_token):
if User.objects.filter(email=email).exists():
user = User.objects.get(email=email)
# Send event to Jitsu for tracking
if settings.ANALYTICS_BASE_API:
_ = requests.post(
settings.ANALYTICS_BASE_API,
headers={
"Content-Type": "application/json",
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
},
json={
"event_id": uuid.uuid4().hex,
"event_data": {
"medium": "code",
},
"user": {"email": email, "id": str(user.id)},
"device_ctx": {
"ip": request.META.get("REMOTE_ADDR"),
"user_agent": request.META.get("HTTP_USER_AGENT"),
},
"event_type": "SIGN_IN",
},
)
else:
user = User.objects.create(
email=email,
username=uuid.uuid4().hex,
password=make_password(uuid.uuid4().hex),
is_password_autoset=True,
)
# Send event to Jitsu for tracking
if settings.ANALYTICS_BASE_API:
_ = requests.post(
settings.ANALYTICS_BASE_API,
headers={
"Content-Type": "application/json",
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
},
json={
"event_id": uuid.uuid4().hex,
"event_data": {
"medium": "code",
},
"user": {"email": email, "id": str(user.id)},
"device_ctx": {
"ip": request.META.get("REMOTE_ADDR"),
"user_agent": request.META.get("HTTP_USER_AGENT"),
},
"event_type": "SIGN_UP",
},
)
user.last_active = timezone.now()
user.last_login_time = timezone.now()
user.last_login_ip = request.META.get("REMOTE_ADDR")
user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
user.token_updated_at = timezone.now()
user.save()
access_token, refresh_token = get_tokens_for_user(user)
data = {
"access_token": access_token,
"refresh_token": refresh_token,
}
return Response(data, status=status.HTTP_200_OK)
else:
return Response(
{"error": "Your login code was incorrect. Please try again."},
status=status.HTTP_400_BAD_REQUEST,
)
else:
return Response(
{"error": "The magic code/link has expired please try again"},
status=status.HTTP_400_BAD_REQUEST,
)
-33
View File
@@ -1,33 +0,0 @@
# Python imports
import os
# Django imports
from django.conf import settings
# Third party imports
from rest_framework.permissions import AllowAny
from rest_framework import status
from rest_framework.response import Response
from sentry_sdk import capture_exception
# Module imports
from .base import BaseAPIView
class ConfigurationEndpoint(BaseAPIView):
permission_classes = [
AllowAny,
]
def get(self, request):
data = {}
data["google"] = os.environ.get("GOOGLE_CLIENT_ID", None)
data["github"] = os.environ.get("GITHUB_CLIENT_ID", None)
data["github_app_name"] = os.environ.get("GITHUB_APP_NAME", None)
data["magic_login"] = (
bool(settings.EMAIL_HOST_USER) and bool(settings.EMAIL_HOST_PASSWORD)
) and os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "0") == "1"
data["email_password_login"] = (
os.environ.get("ENABLE_EMAIL_PASSWORD", "0") == "1"
)
return Response(data, status=status.HTTP_200_OK)
@@ -1,56 +0,0 @@
# Django import
from django.db import IntegrityError
# Third party imports
from rest_framework import status
from rest_framework.response import Response
from sentry_sdk import capture_exception
# Module imports
from plane.api.views import BaseViewSet, BaseAPIView
from plane.db.models import SlackProjectSync, WorkspaceIntegration, ProjectMember
from plane.api.serializers import SlackProjectSyncSerializer
from plane.api.permissions import ProjectBasePermission, ProjectEntityPermission
class SlackProjectSyncViewSet(BaseViewSet):
permission_classes = [
ProjectBasePermission,
]
serializer_class = SlackProjectSyncSerializer
model = SlackProjectSync
def get_queryset(self):
return (
super()
.get_queryset()
.filter(
workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"),
)
.filter(project__project_projectmember__member=self.request.user)
)
def create(self, request, slug, project_id, workspace_integration_id):
serializer = SlackProjectSyncSerializer(data=request.data)
workspace_integration = WorkspaceIntegration.objects.get(
workspace__slug=slug, pk=workspace_integration_id
)
if serializer.is_valid():
serializer.save(
project_id=project_id,
workspace_integration_id=workspace_integration_id,
)
workspace_integration = WorkspaceIntegration.objects.get(
pk=workspace_integration_id, workspace__slug=slug
)
_ = ProjectMember.objects.get_or_create(
member=workspace_integration.actor, role=20, project_id=project_id
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-255
View File
@@ -1,255 +0,0 @@
# Python imports
from datetime import timedelta, datetime, date
# Django imports
from django.db.models import Exists, OuterRef, Q, Prefetch
from django.utils import timezone
# Third party imports
from rest_framework import status
from rest_framework.response import Response
from sentry_sdk import capture_exception
# Module imports
from .base import BaseViewSet, BaseAPIView
from plane.api.permissions import ProjectEntityPermission
from plane.db.models import (
Page,
PageBlock,
PageFavorite,
Issue,
IssueAssignee,
IssueActivity,
)
from plane.api.serializers import (
PageSerializer,
PageBlockSerializer,
PageFavoriteSerializer,
IssueLiteSerializer,
)
class PageViewSet(BaseViewSet):
serializer_class = PageSerializer
model = Page
permission_classes = [
ProjectEntityPermission,
]
search_fields = [
"name",
]
def get_queryset(self):
subquery = PageFavorite.objects.filter(
user=self.request.user,
page_id=OuterRef("pk"),
project_id=self.kwargs.get("project_id"),
workspace__slug=self.kwargs.get("slug"),
)
return self.filter_queryset(
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(project__project_projectmember__member=self.request.user)
.filter(Q(owned_by=self.request.user) | Q(access=0))
.select_related("project")
.select_related("workspace")
.select_related("owned_by")
.annotate(is_favorite=Exists(subquery))
.order_by(self.request.GET.get("order_by", "-created_at"))
.prefetch_related("labels")
.order_by("name", "-is_favorite")
.prefetch_related(
Prefetch(
"blocks",
queryset=PageBlock.objects.select_related(
"page", "issue", "workspace", "project"
),
)
)
.distinct()
)
def perform_create(self, serializer):
serializer.save(
project_id=self.kwargs.get("project_id"), owned_by=self.request.user
)
def create(self, request, slug, project_id):
serializer = PageSerializer(
data=request.data,
context={"project_id": project_id, "owned_by_id": request.user.id},
)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def partial_update(self, request, slug, project_id, pk):
page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
# Only update access if the page owner is the requesting user
if (
page.access != request.data.get("access", page.access)
and page.owned_by_id != request.user.id
):
return Response(
{
"error": "Access cannot be updated since this page is owned by someone else"
},
status=status.HTTP_400_BAD_REQUEST,
)
serializer = PageSerializer(page, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def list(self, request, slug, project_id):
queryset = self.get_queryset()
page_view = request.GET.get("page_view", False)
if not page_view:
return Response({"error": "Page View parameter is required"}, status=status.HTTP_400_BAD_REQUEST)
# All Pages
if page_view == "all":
return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK)
# Recent pages
if page_view == "recent":
current_time = date.today()
day_before = current_time - timedelta(days=1)
todays_pages = queryset.filter(updated_at__date=date.today())
yesterdays_pages = queryset.filter(updated_at__date=day_before)
earlier_this_week = queryset.filter( updated_at__date__range=(
(timezone.now() - timedelta(days=7)),
(timezone.now() - timedelta(days=2)),
))
return Response(
{
"today": PageSerializer(todays_pages, many=True).data,
"yesterday": PageSerializer(yesterdays_pages, many=True).data,
"earlier_this_week": PageSerializer(earlier_this_week, many=True).data,
},
status=status.HTTP_200_OK,
)
# Favorite Pages
if page_view == "favorite":
queryset = queryset.filter(is_favorite=True)
return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK)
# My pages
if page_view == "created_by_me":
queryset = queryset.filter(owned_by=request.user)
return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK)
# Created by other Pages
if page_view == "created_by_other":
queryset = queryset.filter(~Q(owned_by=request.user), access=0)
return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK)
return Response({"error": "No matching view found"}, status=status.HTTP_400_BAD_REQUEST)
class PageBlockViewSet(BaseViewSet):
serializer_class = PageBlockSerializer
model = PageBlock
permission_classes = [
ProjectEntityPermission,
]
def get_queryset(self):
return self.filter_queryset(
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(page_id=self.kwargs.get("page_id"))
.filter(project__project_projectmember__member=self.request.user)
.select_related("project")
.select_related("workspace")
.select_related("page")
.select_related("issue")
.order_by("sort_order")
.distinct()
)
def perform_create(self, serializer):
serializer.save(
project_id=self.kwargs.get("project_id"),
page_id=self.kwargs.get("page_id"),
)
class PageFavoriteViewSet(BaseViewSet):
permission_classes = [
ProjectEntityPermission,
]
serializer_class = PageFavoriteSerializer
model = PageFavorite
def get_queryset(self):
return self.filter_queryset(
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(user=self.request.user)
.select_related("page", "page__owned_by")
)
def create(self, request, slug, project_id):
serializer = PageFavoriteSerializer(data=request.data)
if serializer.is_valid():
serializer.save(user=request.user, project_id=project_id)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def destroy(self, request, slug, project_id, page_id):
page_favorite = PageFavorite.objects.get(
project=project_id,
user=request.user,
workspace__slug=slug,
page_id=page_id,
)
page_favorite.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class CreateIssueFromPageBlockEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def post(self, request, slug, project_id, page_id, page_block_id):
page_block = PageBlock.objects.get(
pk=page_block_id,
workspace__slug=slug,
project_id=project_id,
page_id=page_id,
)
issue = Issue.objects.create(
name=page_block.name,
project_id=project_id,
description=page_block.description,
description_html=page_block.description_html,
description_stripped=page_block.description_stripped,
)
_ = IssueAssignee.objects.create(
issue=issue, assignee=request.user, project_id=project_id
)
_ = IssueActivity.objects.create(
issue=issue,
actor=request.user,
project_id=project_id,
comment=f"created the issue from {page_block.name} block",
verb="created",
)
page_block.issue = issue
page_block.save()
return Response(IssueLiteSerializer(issue).data, status=status.HTTP_200_OK)
+5
View File
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class AppConfig(AppConfig):
name = "plane.app"
@@ -0,0 +1,17 @@
from .workspace import (
WorkSpaceBasePermission,
WorkspaceOwnerPermission,
WorkSpaceAdminPermission,
WorkspaceEntityPermission,
WorkspaceViewerPermission,
WorkspaceUserPermission,
)
from .project import (
ProjectBasePermission,
ProjectEntityPermission,
ProjectMemberPermission,
ProjectLitePermission,
)
@@ -13,14 +13,15 @@ Guest = 5
class ProjectBasePermission(BasePermission):
def has_permission(self, request, view):
if request.user.is_anonymous:
return False
## Safe Methods -> Handle the filtering logic in queryset
if request.method in SAFE_METHODS:
return WorkspaceMember.objects.filter(
workspace__slug=view.workspace_slug, member=request.user
workspace__slug=view.workspace_slug,
member=request.user,
is_active=True,
).exists()
## Only workspace owners or admins can create the projects
@@ -29,6 +30,7 @@ class ProjectBasePermission(BasePermission):
workspace__slug=view.workspace_slug,
member=request.user,
role__in=[Admin, Member],
is_active=True,
).exists()
## Only Project Admins can update project attributes
@@ -37,19 +39,21 @@ class ProjectBasePermission(BasePermission):
member=request.user,
role=Admin,
project_id=view.project_id,
is_active=True,
).exists()
class ProjectMemberPermission(BasePermission):
def has_permission(self, request, view):
if request.user.is_anonymous:
return False
## Safe Methods -> Handle the filtering logic in queryset
if request.method in SAFE_METHODS:
return ProjectMember.objects.filter(
workspace__slug=view.workspace_slug, member=request.user
workspace__slug=view.workspace_slug,
member=request.user,
is_active=True,
).exists()
## Only workspace owners or admins can create the projects
if request.method == "POST":
@@ -57,6 +61,7 @@ class ProjectMemberPermission(BasePermission):
workspace__slug=view.workspace_slug,
member=request.user,
role__in=[Admin, Member],
is_active=True,
).exists()
## Only Project Admins can update project attributes
@@ -65,12 +70,12 @@ class ProjectMemberPermission(BasePermission):
member=request.user,
role__in=[Admin, Member],
project_id=view.project_id,
is_active=True,
).exists()
class ProjectEntityPermission(BasePermission):
def has_permission(self, request, view):
if request.user.is_anonymous:
return False
@@ -80,6 +85,7 @@ class ProjectEntityPermission(BasePermission):
workspace__slug=view.workspace_slug,
member=request.user,
project_id=view.project_id,
is_active=True,
).exists()
## Only project members or admins can create and edit the project attributes
@@ -88,17 +94,18 @@ class ProjectEntityPermission(BasePermission):
member=request.user,
role__in=[Admin, Member],
project_id=view.project_id,
is_active=True,
).exists()
class ProjectLitePermission(BasePermission):
def has_permission(self, request, view):
if request.user.is_anonymous:
return False
return ProjectMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
project_id=view.project_id,
).exists()
is_active=True,
).exists()
@@ -32,15 +32,31 @@ class WorkSpaceBasePermission(BasePermission):
member=request.user,
workspace__slug=view.workspace_slug,
role__in=[Owner, Admin],
is_active=True,
).exists()
# allow only owner to delete the workspace
if request.method == "DELETE":
return WorkspaceMember.objects.filter(
member=request.user, workspace__slug=view.workspace_slug, role=Owner
member=request.user,
workspace__slug=view.workspace_slug,
role=Owner,
is_active=True,
).exists()
class WorkspaceOwnerPermission(BasePermission):
def has_permission(self, request, view):
if request.user.is_anonymous:
return False
return WorkspaceMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
role=Owner,
).exists()
class WorkSpaceAdminPermission(BasePermission):
def has_permission(self, request, view):
if request.user.is_anonymous:
@@ -50,6 +66,7 @@ class WorkSpaceAdminPermission(BasePermission):
member=request.user,
workspace__slug=view.workspace_slug,
role__in=[Owner, Admin],
is_active=True,
).exists()
@@ -63,12 +80,14 @@ class WorkspaceEntityPermission(BasePermission):
return WorkspaceMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
is_active=True,
).exists()
return WorkspaceMember.objects.filter(
member=request.user,
workspace__slug=view.workspace_slug,
role__in=[Owner, Admin],
is_active=True,
).exists()
@@ -78,5 +97,20 @@ class WorkspaceViewerPermission(BasePermission):
return False
return WorkspaceMember.objects.filter(
member=request.user, workspace__slug=view.workspace_slug, role__gte=10
member=request.user,
workspace__slug=view.workspace_slug,
role__gte=10,
is_active=True,
).exists()
class WorkspaceUserPermission(BasePermission):
def has_permission(self, request, view):
if request.user.is_anonymous:
return False
return WorkspaceMember.objects.filter(
member=request.user,
workspace__slug=view.workspace_slug,
is_active=True,
).exists()
@@ -71,7 +71,7 @@ from .module import (
ModuleFavoriteSerializer,
)
from .api_token import APITokenSerializer
from .api import APITokenSerializer, APITokenReadSerializer
from .integration import (
IntegrationSerializer,
@@ -85,7 +85,7 @@ from .integration import (
from .importer import ImporterSerializer
from .page import PageSerializer, PageBlockSerializer, PageFavoriteSerializer
from .page import PageSerializer, PageLogSerializer, SubPageSerializer, PageFavoriteSerializer
from .estimate import (
EstimateSerializer,
@@ -100,3 +100,5 @@ from .analytic import AnalyticViewSerializer
from .notification import NotificationSerializer
from .exporter import ExporterHistorySerializer
from .webhook import WebhookSerializer, WebhookLogSerializer
@@ -17,7 +17,7 @@ class AnalyticViewSerializer(BaseSerializer):
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
validated_data["query"] = dict()
validated_data["query"] = {}
return AnalyticView.objects.create(**validated_data)
def update(self, instance, validated_data):
@@ -25,6 +25,6 @@ class AnalyticViewSerializer(BaseSerializer):
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
validated_data["query"] = dict()
validated_data["query"] = {}
validated_data["query"] = issue_filters(query_params, "PATCH")
return super().update(instance, validated_data)
+31
View File
@@ -0,0 +1,31 @@
from .base import BaseSerializer
from plane.db.models import APIToken, APIActivityLog
class APITokenSerializer(BaseSerializer):
class Meta:
model = APIToken
fields = "__all__"
read_only_fields = [
"token",
"expired_at",
"created_at",
"updated_at",
"workspace",
"user",
]
class APITokenReadSerializer(BaseSerializer):
class Meta:
model = APIToken
exclude = ('token',)
class APIActivityLogSerializer(BaseSerializer):
class Meta:
model = APIActivityLog
fields = "__all__"
@@ -1,6 +1,3 @@
# Django imports
from django.db.models.functions import TruncDate
# Third party imports
from rest_framework import serializers
@@ -2,7 +2,7 @@
from .base import BaseSerializer
from plane.db.models import Estimate, EstimatePoint
from plane.api.serializers import WorkspaceLiteSerializer, ProjectLiteSerializer
from plane.app.serializers import WorkspaceLiteSerializer, ProjectLiteSerializer
class EstimateSerializer(BaseSerializer):
@@ -6,7 +6,6 @@ from .base import BaseSerializer
from .issue import IssueFlatSerializer, LabelLiteSerializer
from .project import ProjectLiteSerializer
from .state import StateLiteSerializer
from .project import ProjectLiteSerializer
from .user import UserLiteSerializer
from plane.db.models import Inbox, InboxIssue, Issue
@@ -5,4 +5,4 @@ from .github import (
GithubIssueSyncSerializer,
GithubCommentSyncSerializer,
)
from .slack import SlackProjectSyncSerializer
from .slack import SlackProjectSyncSerializer
@@ -1,5 +1,5 @@
# Module imports
from plane.api.serializers import BaseSerializer
from plane.app.serializers import BaseSerializer
from plane.db.models import Integration, WorkspaceIntegration
@@ -1,5 +1,5 @@
# Module imports
from plane.api.serializers import BaseSerializer
from plane.app.serializers import BaseSerializer
from plane.db.models import (
GithubIssueSync,
GithubRepository,
@@ -1,5 +1,5 @@
# Module imports
from plane.api.serializers import BaseSerializer
from plane.app.serializers import BaseSerializer
from plane.db.models import SlackProjectSync
@@ -5,11 +5,10 @@ from django.utils import timezone
from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .base import BaseSerializer, DynamicBaseSerializer
from .user import UserLiteSerializer
from .state import StateSerializer, StateLiteSerializer
from .user import UserLiteSerializer
from .project import ProjectSerializer, ProjectLiteSerializer
from .project import ProjectLiteSerializer
from .workspace import WorkspaceLiteSerializer
from plane.db.models import (
User,
@@ -232,25 +231,6 @@ class IssueActivitySerializer(BaseSerializer):
fields = "__all__"
class IssueCommentSerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor")
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
class Meta:
model = IssueComment
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"issue",
"created_by",
"updated_by",
"created_at",
"updated_at",
]
class IssuePropertySerializer(BaseSerializer):
class Meta:
@@ -287,7 +267,6 @@ class LabelLiteSerializer(BaseSerializer):
class IssueLabelSerializer(BaseSerializer):
# label_details = LabelSerializer(read_only=True, source="label")
class Meta:
model = IssueLabel
@@ -526,7 +505,7 @@ class IssueStateFlatSerializer(BaseSerializer):
# Issue Serializer with state details
class IssueStateSerializer(BaseSerializer):
class IssueStateSerializer(DynamicBaseSerializer):
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
state_detail = StateLiteSerializer(read_only=True, source="state")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
@@ -569,7 +548,7 @@ class IssueSerializer(BaseSerializer):
]
class IssueLiteSerializer(BaseSerializer):
class IssueLiteSerializer(DynamicBaseSerializer):
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
state_detail = StateLiteSerializer(read_only=True, source="state")
@@ -4,9 +4,8 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .user import UserLiteSerializer
from .project import ProjectSerializer, ProjectLiteSerializer
from .project import ProjectLiteSerializer
from .workspace import WorkspaceLiteSerializer
from .issue import IssueStateSerializer
from plane.db.models import (
User,
@@ -19,7 +18,7 @@ from plane.db.models import (
class ModuleWriteSerializer(BaseSerializer):
members_list = serializers.ListField(
members = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
write_only=True,
required=False,
@@ -39,6 +38,11 @@ class ModuleWriteSerializer(BaseSerializer):
"created_at",
"updated_at",
]
def to_representation(self, instance):
data = super().to_representation(instance)
data['members'] = [str(member.id) for member in instance.members.all()]
return data
def validate(self, data):
if data.get("start_date", None) is not None and data.get("target_date", None) is not None and data.get("start_date", None) > data.get("target_date", None):
@@ -46,7 +50,7 @@ class ModuleWriteSerializer(BaseSerializer):
return data
def create(self, validated_data):
members = validated_data.pop("members_list", None)
members = validated_data.pop("members", None)
project = self.context["project"]
@@ -72,7 +76,7 @@ class ModuleWriteSerializer(BaseSerializer):
return module
def update(self, instance, validated_data):
members = validated_data.pop("members_list", None)
members = validated_data.pop("members", None)
if members is not None:
ModuleMember.objects.filter(module=instance).delete()
@@ -6,39 +6,17 @@ from .base import BaseSerializer
from .issue import IssueFlatSerializer, LabelLiteSerializer
from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer
from plane.db.models import Page, PageBlock, PageFavorite, PageLabel, Label
class PageBlockSerializer(BaseSerializer):
issue_detail = IssueFlatSerializer(source="issue", read_only=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
class Meta:
model = PageBlock
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"page",
]
class PageBlockLiteSerializer(BaseSerializer):
class Meta:
model = PageBlock
fields = "__all__"
from plane.db.models import Page, PageLog, PageFavorite, PageLabel, Label, Issue, Module
class PageSerializer(BaseSerializer):
is_favorite = serializers.BooleanField(read_only=True)
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
labels_list = serializers.ListField(
labels = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
write_only=True,
required=False,
)
blocks = PageBlockLiteSerializer(read_only=True, many=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
@@ -50,9 +28,13 @@ class PageSerializer(BaseSerializer):
"project",
"owned_by",
]
def to_representation(self, instance):
data = super().to_representation(instance)
data['labels'] = [str(label.id) for label in instance.labels.all()]
return data
def create(self, validated_data):
labels = validated_data.pop("labels_list", None)
labels = validated_data.pop("labels", None)
project_id = self.context["project_id"]
owned_by_id = self.context["owned_by_id"]
page = Page.objects.create(
@@ -77,7 +59,7 @@ class PageSerializer(BaseSerializer):
return page
def update(self, instance, validated_data):
labels = validated_data.pop("labels_list", None)
labels = validated_data.pop("labels", None)
if labels is not None:
PageLabel.objects.filter(page=instance).delete()
PageLabel.objects.bulk_create(
@@ -98,6 +80,41 @@ class PageSerializer(BaseSerializer):
return super().update(instance, validated_data)
class SubPageSerializer(BaseSerializer):
entity_details = serializers.SerializerMethodField()
class Meta:
model = PageLog
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"page",
]
def get_entity_details(self, obj):
entity_name = obj.entity_name
if entity_name == 'forward_link' or entity_name == 'back_link':
try:
page = Page.objects.get(pk=obj.entity_identifier)
return PageSerializer(page).data
except Page.DoesNotExist:
return None
return None
class PageLogSerializer(BaseSerializer):
class Meta:
model = PageLog
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"page",
]
class PageFavoriteSerializer(BaseSerializer):
page_detail = PageSerializer(source="page", read_only=True)
@@ -3,8 +3,8 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer, DynamicBaseSerializer
from plane.api.serializers.workspace import WorkSpaceSerializer, WorkspaceLiteSerializer
from plane.api.serializers.user import UserLiteSerializer, UserAdminLiteSerializer
from plane.app.serializers.workspace import WorkspaceLiteSerializer
from plane.app.serializers.user import UserLiteSerializer, UserAdminLiteSerializer
from plane.db.models import (
Project,
ProjectMember,
@@ -103,13 +103,16 @@ class ProjectListSerializer(DynamicBaseSerializer):
members = serializers.SerializerMethodField()
def get_members(self, obj):
project_members = ProjectMember.objects.filter(project_id=obj.id).values(
project_members = ProjectMember.objects.filter(
project_id=obj.id,
is_active=True,
).values(
"id",
"member_id",
"member__display_name",
"member__avatar",
)
return project_members
return list(project_members)
class Meta:
model = Project
@@ -1,14 +1,11 @@
# Module imports
from .base import BaseSerializer
from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer
from plane.db.models import State
class StateSerializer(BaseSerializer):
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
class Meta:
model = State
@@ -4,6 +4,7 @@ from rest_framework import serializers
# Module import
from .base import BaseSerializer
from plane.db.models import User, Workspace, WorkspaceMemberInvite
from plane.license.models import InstanceAdmin, Instance
class UserSerializer(BaseSerializer):
@@ -79,14 +80,16 @@ class UserMeSettingsSerializer(BaseSerializer):
email=obj.email
).count()
if obj.last_workspace_id is not None:
workspace = Workspace.objects.get(
workspace = Workspace.objects.filter(
pk=obj.last_workspace_id, workspace_member__member=obj.id
)
).first()
return {
"last_workspace_id": obj.last_workspace_id,
"last_workspace_slug": workspace.slug,
"last_workspace_slug": workspace.slug if workspace is not None else "",
"fallback_workspace_id": obj.last_workspace_id,
"fallback_workspace_slug": workspace.slug,
"fallback_workspace_slug": workspace.slug
if workspace is not None
else "",
"invites": workspace_invites,
}
else:
@@ -57,7 +57,7 @@ class IssueViewSerializer(BaseSerializer):
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
validated_data["query"] = dict()
validated_data["query"] = {}
return IssueView.objects.create(**validated_data)
def update(self, instance, validated_data):
@@ -65,7 +65,7 @@ class IssueViewSerializer(BaseSerializer):
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
validated_data["query"] = dict()
validated_data["query"] = {}
validated_data["query"] = issue_filters(query_params, "PATCH")
return super().update(instance, validated_data)
@@ -0,0 +1,30 @@
# Third party imports
from rest_framework import serializers
# Module imports
from .base import DynamicBaseSerializer
from plane.db.models import Webhook, WebhookLog
from plane.db.models.webhook import validate_domain, validate_schema
class WebhookSerializer(DynamicBaseSerializer):
url = serializers.URLField(validators=[validate_schema, validate_domain])
class Meta:
model = Webhook
fields = "__all__"
read_only_fields = [
"workspace",
"secret_key",
]
class WebhookLogSerializer(DynamicBaseSerializer):
class Meta:
model = WebhookLog
fields = "__all__"
read_only_fields = [
"workspace",
"webhook"
]
@@ -110,9 +110,8 @@ class TeamSerializer(BaseSerializer):
]
TeamMember.objects.bulk_create(team_members, batch_size=10)
return team
else:
team = Team.objects.create(**validated_data)
return team
team = Team.objects.create(**validated_data)
return team
def update(self, instance, validated_data):
if "members" in validated_data:
@@ -124,8 +123,7 @@ class TeamSerializer(BaseSerializer):
]
TeamMember.objects.bulk_create(team_members, batch_size=10)
return super().update(instance, validated_data)
else:
return super().update(instance, validated_data)
return super().update(instance, validated_data)
class WorkspaceThemeSerializer(BaseSerializer):
@@ -1,10 +1,10 @@
from .analytic import urlpatterns as analytic_urls
from .asset import urlpatterns as asset_urls
from .authentication import urlpatterns as authentication_urls
from .configuration import urlpatterns as configuration_urls
from .config import urlpatterns as configuration_urls
from .cycle import urlpatterns as cycle_urls
from .estimate import urlpatterns as estimate_urls
from .gpt import urlpatterns as gpt_urls
from .external import urlpatterns as external_urls
from .importer import urlpatterns as importer_urls
from .inbox import urlpatterns as inbox_urls
from .integration import urlpatterns as integration_urls
@@ -13,14 +13,13 @@ from .module import urlpatterns as module_urls
from .notification import urlpatterns as notification_urls
from .page import urlpatterns as page_urls
from .project import urlpatterns as project_urls
from .public_board import urlpatterns as public_board_urls
from .release_note import urlpatterns as release_note_urls
from .search import urlpatterns as search_urls
from .state import urlpatterns as state_urls
from .unsplash import urlpatterns as unsplash_urls
from .user import urlpatterns as user_urls
from .views import urlpatterns as view_urls
from .workspace import urlpatterns as workspace_urls
from .api import urlpatterns as api_urls
from .webhook import urlpatterns as webhook_urls
urlpatterns = [
@@ -30,7 +29,7 @@ urlpatterns = [
*configuration_urls,
*cycle_urls,
*estimate_urls,
*gpt_urls,
*external_urls,
*importer_urls,
*inbox_urls,
*integration_urls,
@@ -39,12 +38,11 @@ urlpatterns = [
*notification_urls,
*page_urls,
*project_urls,
*public_board_urls,
*release_note_urls,
*search_urls,
*state_urls,
*unsplash_urls,
*user_urls,
*view_urls,
*workspace_urls,
*api_urls,
*webhook_urls,
]
@@ -1,7 +1,7 @@
from django.urls import path
from plane.api.views import (
from plane.app.views import (
AnalyticsEndpoint,
AnalyticViewViewset,
SavedAnalyticEndpoint,
+17
View File
@@ -0,0 +1,17 @@
from django.urls import path
from plane.app.views import ApiTokenEndpoint
urlpatterns = [
# API Tokens
path(
"workspaces/<str:slug>/api-tokens/",
ApiTokenEndpoint.as_view(),
name="api-tokens",
),
path(
"workspaces/<str:slug>/api-tokens/<uuid:pk>/",
ApiTokenEndpoint.as_view(),
name="api-tokens",
),
## End API Tokens
]
@@ -1,7 +1,7 @@
from django.urls import path
from plane.api.views import (
from plane.app.views import (
FileAssetEndpoint,
UserAssetsEndpoint,
)
@@ -3,7 +3,7 @@ from django.urls import path
from rest_framework_simplejwt.views import TokenRefreshView
from plane.api.views import (
from plane.app.views import (
# Authentication
SignUpEndpoint,
SignInEndpoint,
@@ -1,7 +1,7 @@
from django.urls import path
from plane.api.views import ConfigurationEndpoint
from plane.app.views import ConfigurationEndpoint
urlpatterns = [
path(
@@ -1,12 +1,13 @@
from django.urls import path
from plane.api.views import (
from plane.app.views import (
CycleViewSet,
CycleIssueViewSet,
CycleDateCheckEndpoint,
CycleFavoriteViewSet,
TransferCycleIssueEndpoint,
CycleIssueGroupedEndpoint,
)
@@ -43,6 +44,11 @@ urlpatterns = [
),
name="project-issue-cycle",
),
path(
"v3/workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/",
CycleIssueGroupedEndpoint.as_view(),
name="project-issue-cycle",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/<uuid:pk>/",
CycleIssueViewSet.as_view(
@@ -1,7 +1,7 @@
from django.urls import path
from plane.api.views import (
from plane.app.views import (
ProjectEstimatePointEndpoint,
BulkEstimatePointEndpoint,
)
+25
View File
@@ -0,0 +1,25 @@
from django.urls import path
from plane.app.views import UnsplashEndpoint
from plane.app.views import ReleaseNotesEndpoint
from plane.app.views import GPTIntegrationEndpoint
urlpatterns = [
path(
"unsplash/",
UnsplashEndpoint.as_view(),
name="unsplash",
),
path(
"release-notes/",
ReleaseNotesEndpoint.as_view(),
name="release-notes",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/ai-assistant/",
GPTIntegrationEndpoint.as_view(),
name="importer",
),
]
@@ -1,7 +1,7 @@
from django.urls import path
from plane.api.views import (
from plane.app.views import (
ServiceIssueImportSummaryEndpoint,
ImportServiceEndpoint,
UpdateServiceImportStatusEndpoint,
@@ -1,7 +1,7 @@
from django.urls import path
from plane.api.views import (
from plane.app.views import (
InboxViewSet,
InboxIssueViewSet,
)
@@ -1,7 +1,7 @@
from django.urls import path
from plane.api.views import (
from plane.app.views import (
IntegrationViewSet,
WorkspaceIntegrationViewSet,
GithubRepositoriesEndpoint,
@@ -1,8 +1,10 @@
from django.urls import path
from plane.api.views import (
from plane.app.views import (
IssueViewSet,
IssueListEndpoint,
IssueListGroupedEndpoint,
LabelViewSet,
BulkCreateIssueLabelsEndpoint,
BulkDeleteIssuesEndpoint,
@@ -35,6 +37,16 @@ urlpatterns = [
),
name="project-issue",
),
path(
"v2/workspaces/<str:slug>/projects/<uuid:project_id>/issues/",
IssueListEndpoint.as_view(),
name="project-issue",
),
path(
"v3/workspaces/<str:slug>/projects/<uuid:project_id>/issues/",
IssueListGroupedEndpoint.as_view(),
name="project-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:pk>/",
IssueViewSet.as_view(
@@ -1,12 +1,13 @@
from django.urls import path
from plane.api.views import (
from plane.app.views import (
ModuleViewSet,
ModuleIssueViewSet,
ModuleLinkViewSet,
ModuleFavoriteViewSet,
BulkImportModulesEndpoint,
ModuleIssueGroupedEndpoint,
)
@@ -43,6 +44,11 @@ urlpatterns = [
),
name="project-module-issues",
),
path(
"v3/workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/",
ModuleIssueGroupedEndpoint.as_view(),
name="project-issue-cycle",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/<uuid:pk>/",
ModuleIssueViewSet.as_view(
@@ -1,7 +1,7 @@
from django.urls import path
from plane.api.views import (
from plane.app.views import (
NotificationViewSet,
UnreadNotificationEndpoint,
MarkAllReadNotificationViewSet,
+133
View File
@@ -0,0 +1,133 @@
from django.urls import path
from plane.app.views import (
PageViewSet,
PageFavoriteViewSet,
PageLogEndpoint,
SubPagesEndpoint,
)
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/",
PageViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/",
PageViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-pages/",
PageFavoriteViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="user-favorite-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-pages/<uuid:page_id>/",
PageFavoriteViewSet.as_view(
{
"delete": "destroy",
}
),
name="user-favorite-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/",
PageViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/",
PageViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/archive/",
PageViewSet.as_view(
{
"post": "archive",
}
),
name="project-page-archive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/unarchive/",
PageViewSet.as_view(
{
"post": "unarchive",
}
),
name="project-page-unarchive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-pages/",
PageViewSet.as_view(
{
"get": "archive_list",
}
),
name="project-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/lock/",
PageViewSet.as_view(
{
"post": "lock",
}
),
name="project-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/unlock/",
PageViewSet.as_view(
{
"post": "unlock",
}
),
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/",
PageLogEndpoint.as_view(),
name="page-transactions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/<uuid:transaction>/",
PageLogEndpoint.as_view(),
name="page-transactions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/sub-pages/",
SubPagesEndpoint.as_view(),
name="sub-page",
),
]
@@ -1,20 +1,18 @@
from django.urls import path
from plane.api.views import (
from plane.app.views import (
ProjectViewSet,
InviteProjectEndpoint,
ProjectInvitationsViewset,
ProjectMemberViewSet,
ProjectMemberEndpoint,
ProjectMemberInvitationsViewset,
ProjectMemberUserEndpoint,
AddMemberToProjectEndpoint,
ProjectJoinEndpoint,
AddTeamToProjectEndpoint,
ProjectUserViewsEndpoint,
ProjectIdentifierEndpoint,
ProjectFavoritesViewSet,
LeaveProjectEndpoint,
ProjectPublicCoverImagesEndpoint
UserProjectInvitationsViewset,
ProjectPublicCoverImagesEndpoint,
ProjectDeployBoardViewSet,
)
@@ -47,13 +45,48 @@ urlpatterns = [
name="project-identifiers",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/invite/",
InviteProjectEndpoint.as_view(),
name="invite-project",
"workspaces/<str:slug>/projects/<uuid:project_id>/invitations/",
ProjectInvitationsViewset.as_view(
{
"get": "list",
"post": "create",
},
),
name="project-member-invite",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/invitations/<uuid:pk>/",
ProjectInvitationsViewset.as_view(
{
"get": "retrieve",
"delete": "destroy",
}
),
name="project-member-invite",
),
path(
"users/me/invitations/projects/",
UserProjectInvitationsViewset.as_view(
{
"get": "list",
"post": "create",
},
),
name="user-project-invitations",
),
path(
"workspaces/<str:slug>/projects/join/",
ProjectJoinEndpoint.as_view(),
name="project-join",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/members/",
ProjectMemberViewSet.as_view({"get": "list"}),
ProjectMemberViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-member",
),
path(
@@ -68,40 +101,19 @@ urlpatterns = [
name="project-member",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-members/",
ProjectMemberEndpoint.as_view(),
"workspaces/<str:slug>/projects/<uuid:project_id>/members/leave/",
ProjectMemberViewSet.as_view(
{
"post": "leave",
}
),
name="project-member",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/members/add/",
AddMemberToProjectEndpoint.as_view(),
name="project",
),
path(
"workspaces/<str:slug>/projects/join/",
ProjectJoinEndpoint.as_view(),
name="project-join",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/team-invite/",
AddTeamToProjectEndpoint.as_view(),
name="projects",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/invitations/",
ProjectMemberInvitationsViewset.as_view({"get": "list"}),
name="project-member-invite",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/invitations/<uuid:pk>/",
ProjectMemberInvitationsViewset.as_view(
{
"get": "retrieve",
"delete": "destroy",
}
),
name="project-member-invite",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-views/",
ProjectUserViewsEndpoint.as_view(),
@@ -131,14 +143,30 @@ urlpatterns = [
),
name="project-favorite",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/members/leave/",
LeaveProjectEndpoint.as_view(),
name="leave-project",
),
path(
"project-covers/",
ProjectPublicCoverImagesEndpoint.as_view(),
name="project-covers",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/",
ProjectDeployBoardViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-deploy-board",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/<uuid:pk>/",
ProjectDeployBoardViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-deploy-board",
),
]
@@ -1,7 +1,7 @@
from django.urls import path
from plane.api.views import (
from plane.app.views import (
GlobalSearchEndpoint,
IssueSearchEndpoint,
)
@@ -1,7 +1,7 @@
from django.urls import path
from plane.api.views import StateViewSet
from plane.app.views import StateViewSet
urlpatterns = [
@@ -20,11 +20,19 @@ urlpatterns = [
StateViewSet.as_view(
{
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-state",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/states/<uuid:pk>/mark-default/",
StateViewSet.as_view(
{
"post": "mark_as_default",
}
),
name="project-state",
),
]
@@ -1,6 +1,6 @@
from django.urls import path
from plane.api.views import (
from plane.app.views import (
## User
UserEndpoint,
UpdateUserOnBoardedEndpoint,
@@ -9,15 +9,10 @@ from plane.api.views import (
ChangePasswordEndpoint,
## End User
## Workspaces
UserWorkspaceInvitationsEndpoint,
UserWorkSpacesEndpoint,
JoinWorkspaceEndpoint,
UserWorkspaceInvitationsEndpoint,
UserWorkspaceInvitationEndpoint,
UserActivityGraphEndpoint,
UserIssueCompletedGraphEndpoint,
UserWorkspaceDashboardEndpoint,
UserProjectInvitationsViewset,
## End Workspaces
)
@@ -26,7 +21,11 @@ urlpatterns = [
path(
"users/me/",
UserEndpoint.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
{
"get": "retrieve",
"patch": "partial_update",
"delete": "deactivate",
}
),
name="users",
),
@@ -39,6 +38,15 @@ urlpatterns = [
),
name="users",
),
path(
"users/me/instance-admin/",
UserEndpoint.as_view(
{
"get": "retrieve_instance_admin",
}
),
name="users",
),
path(
"users/me/change-password/",
ChangePasswordEndpoint.as_view(),
@@ -65,23 +73,6 @@ urlpatterns = [
UserWorkSpacesEndpoint.as_view(),
name="user-workspace",
),
# user workspace invitations
path(
"users/me/invitations/workspaces/",
UserWorkspaceInvitationsEndpoint.as_view({"get": "list", "post": "create"}),
name="user-workspace-invitations",
),
# user workspace invitation
path(
"users/me/invitations/<uuid:pk>/",
UserWorkspaceInvitationEndpoint.as_view(
{
"get": "retrieve",
}
),
name="user-workspace-invitation",
),
# user join workspace
# User Graphs
path(
"users/me/workspaces/<str:slug>/activity-graph/",
@@ -99,15 +90,4 @@ urlpatterns = [
name="user-workspace-dashboard",
),
## End User Graph
path(
"users/me/invitations/workspaces/<str:slug>/<uuid:pk>/join/",
JoinWorkspaceEndpoint.as_view(),
name="user-join-workspace",
),
# user project invitations
path(
"users/me/invitations/projects/",
UserProjectInvitationsViewset.as_view({"get": "list", "post": "create"}),
name="user-project-invitations",
),
]
@@ -1,7 +1,7 @@
from django.urls import path
from plane.api.views import (
from plane.app.views import (
IssueViewViewSet,
GlobalViewViewSet,
GlobalViewIssuesViewSet,
+31
View File
@@ -0,0 +1,31 @@
from django.urls import path
from plane.app.views import (
WebhookEndpoint,
WebhookLogsEndpoint,
WebhookSecretRegenerateEndpoint,
)
urlpatterns = [
path(
"workspaces/<str:slug>/webhooks/",
WebhookEndpoint.as_view(),
name="webhooks",
),
path(
"workspaces/<str:slug>/webhooks/<uuid:pk>/",
WebhookEndpoint.as_view(),
name="webhooks",
),
path(
"workspaces/<str:slug>/webhooks/<uuid:pk>/regenerate/",
WebhookSecretRegenerateEndpoint.as_view(),
name="webhooks",
),
path(
"workspaces/<str:slug>/webhook-logs/<uuid:webhook_id>/",
WebhookLogsEndpoint.as_view(),
name="webhooks",
),
]
@@ -1,11 +1,11 @@
from django.urls import path
from plane.api.views import (
from plane.app.views import (
UserWorkspaceInvitationsViewSet,
WorkSpaceViewSet,
InviteWorkspaceEndpoint,
WorkspaceJoinEndpoint,
WorkSpaceMemberViewSet,
WorkspaceMembersEndpoint,
WorkspaceInvitationsViewset,
WorkspaceMemberUserEndpoint,
WorkspaceMemberUserViewsEndpoint,
@@ -18,7 +18,6 @@ from plane.api.views import (
WorkspaceUserProfileEndpoint,
WorkspaceUserProfileIssuesEndpoint,
WorkspaceLabelsEndpoint,
LeaveWorkspaceEndpoint,
)
@@ -50,14 +49,14 @@ urlpatterns = [
),
name="workspace",
),
path(
"workspaces/<str:slug>/invite/",
InviteWorkspaceEndpoint.as_view(),
name="invite-workspace",
),
path(
"workspaces/<str:slug>/invitations/",
WorkspaceInvitationsViewset.as_view({"get": "list"}),
WorkspaceInvitationsViewset.as_view(
{
"get": "list",
"post": "create",
},
),
name="workspace-invitations",
),
path(
@@ -70,6 +69,23 @@ urlpatterns = [
),
name="workspace-invitations",
),
# user workspace invitations
path(
"users/me/workspaces/invitations/",
UserWorkspaceInvitationsViewSet.as_view(
{
"get": "list",
"post": "create",
},
),
name="user-workspace-invitations",
),
path(
"workspaces/<str:slug>/invitations/<uuid:pk>/join/",
WorkspaceJoinEndpoint.as_view(),
name="workspace-join",
),
# user join workspace
path(
"workspaces/<str:slug>/members/",
WorkSpaceMemberViewSet.as_view({"get": "list"}),
@@ -87,9 +103,13 @@ urlpatterns = [
name="workspace-member",
),
path(
"workspaces/<str:slug>/workspace-members/",
WorkspaceMembersEndpoint.as_view(),
name="workspace-members",
"workspaces/<str:slug>/members/leave/",
WorkSpaceMemberViewSet.as_view(
{
"post": "leave",
},
),
name="leave-workspace-members",
),
path(
"workspaces/<str:slug>/teams/",
@@ -174,9 +194,4 @@ urlpatterns = [
WorkspaceLabelsEndpoint.as_view(),
name="workspace-labels",
),
path(
"workspaces/<str:slug>/members/leave/",
LeaveWorkspaceEndpoint.as_view(),
name="leave-workspace-members",
),
]
@@ -4,7 +4,7 @@ from rest_framework_simplejwt.views import TokenRefreshView
# Create your urls here.
from plane.api.views import (
from plane.app.views import (
# Authentication
SignUpEndpoint,
SignInEndpoint,
@@ -28,7 +28,6 @@ from plane.api.views import (
## End User
# Workspaces
WorkSpaceViewSet,
UserWorkspaceInvitationsEndpoint,
UserWorkSpacesEndpoint,
InviteWorkspaceEndpoint,
JoinWorkspaceEndpoint,
@@ -125,9 +124,10 @@ from plane.api.views import (
## End Modules
# Pages
PageViewSet,
PageBlockViewSet,
PageLogEndpoint,
SubPagesEndpoint,
PageFavoriteViewSet,
CreateIssueFromPageBlockEndpoint,
CreateIssueFromBlockEndpoint,
## End Pages
# Api Tokens
ApiTokenEndpoint,
@@ -1223,25 +1223,81 @@ urlpatterns = [
name="project-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/page-blocks/",
PageBlockViewSet.as_view(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/archive/",
PageViewSet.as_view(
{
"post": "archive",
}
),
name="project-page-archive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/unarchive/",
PageViewSet.as_view(
{
"post": "unarchive",
}
),
name="project-page-unarchive"
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-pages/",
PageViewSet.as_view(
{
"get": "archive_list",
}
),
name="project-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/lock/",
PageViewSet.as_view(
{
"post": "lock",
}
),
name="project-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/unlock/",
PageViewSet.as_view(
{
"post": "unlock",
}
)
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/",
PageLogEndpoint.as_view(), name="page-transactions"
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/<uuid:transaction>/",
PageLogEndpoint.as_view(), name="page-transactions"
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/sub-pages/",
SubPagesEndpoint.as_view(), name="sub-page"
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
BulkEstimatePointEndpoint.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-page-blocks",
name="bulk-create-estimate-points",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/page-blocks/<uuid:pk>/",
PageBlockViewSet.as_view(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/",
BulkEstimatePointEndpoint.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-page-blocks",
name="bulk-create-estimate-points",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-pages/",
@@ -1264,7 +1320,7 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/page-blocks/<uuid:page_block_id>/issues/",
CreateIssueFromPageBlockEndpoint.as_view(),
CreateIssueFromBlockEndpoint.as_view(),
name="page-block-issues",
),
## End Pages
@@ -2,22 +2,15 @@ from .project import (
ProjectViewSet,
ProjectMemberViewSet,
UserProjectInvitationsViewset,
InviteProjectEndpoint,
ProjectInvitationsViewset,
AddTeamToProjectEndpoint,
ProjectMemberInvitationsViewset,
ProjectMemberInviteDetailViewSet,
ProjectIdentifierEndpoint,
AddMemberToProjectEndpoint,
ProjectJoinEndpoint,
ProjectUserViewsEndpoint,
ProjectMemberUserEndpoint,
ProjectFavoritesViewSet,
ProjectDeployBoardViewSet,
ProjectDeployBoardPublicSettingsEndpoint,
ProjectMemberEndpoint,
WorkspaceProjectDeployBoardEndpoint,
LeaveProjectEndpoint,
ProjectPublicCoverImagesEndpoint,
ProjectDeployBoardViewSet,
)
from .user import (
UserEndpoint,
@@ -28,19 +21,17 @@ from .user import (
from .oauth import OauthEndpoint
from .base import BaseAPIView, BaseViewSet
from .base import BaseAPIView, BaseViewSet, WebhookMixin
from .workspace import (
WorkSpaceViewSet,
UserWorkSpacesEndpoint,
WorkSpaceAvailabilityCheckEndpoint,
InviteWorkspaceEndpoint,
JoinWorkspaceEndpoint,
WorkspaceJoinEndpoint,
WorkSpaceMemberViewSet,
TeamMemberViewSet,
WorkspaceInvitationsViewset,
UserWorkspaceInvitationsEndpoint,
UserWorkspaceInvitationEndpoint,
UserWorkspaceInvitationsViewSet,
UserLastProjectWithWorkspaceEndpoint,
WorkspaceMemberUserEndpoint,
WorkspaceMemberUserViewsEndpoint,
@@ -53,21 +44,27 @@ from .workspace import (
WorkspaceUserProfileEndpoint,
WorkspaceUserProfileIssuesEndpoint,
WorkspaceLabelsEndpoint,
WorkspaceMembersEndpoint,
LeaveWorkspaceEndpoint,
)
from .state import StateViewSet
from .view import GlobalViewViewSet, GlobalViewIssuesViewSet, IssueViewViewSet, IssueViewFavoriteViewSet
from .view import (
GlobalViewViewSet,
GlobalViewIssuesViewSet,
IssueViewViewSet,
IssueViewFavoriteViewSet,
)
from .cycle import (
CycleViewSet,
CycleIssueViewSet,
CycleDateCheckEndpoint,
CycleFavoriteViewSet,
TransferCycleIssueEndpoint,
CycleIssueGroupedEndpoint,
)
from .asset import FileAssetEndpoint, UserAssetsEndpoint
from .issue import (
IssueViewSet,
IssueListEndpoint,
IssueListGroupedEndpoint,
WorkSpaceIssuesEndpoint,
IssueActivityEndpoint,
IssueCommentViewSet,
@@ -81,15 +78,9 @@ from .issue import (
IssueAttachmentEndpoint,
IssueArchiveViewSet,
IssueSubscriberViewSet,
IssueCommentPublicViewSet,
CommentReactionViewSet,
IssueReactionViewSet,
IssueReactionPublicViewSet,
CommentReactionPublicViewSet,
IssueVotePublicViewSet,
IssueRelationViewSet,
IssueRetrievePublicEndpoint,
ProjectIssuesPublicEndpoint,
IssueDraftViewSet,
)
@@ -115,9 +106,10 @@ from .module import (
ModuleIssueViewSet,
ModuleLinkViewSet,
ModuleFavoriteViewSet,
ModuleIssueGroupedEndpoint,
)
from .api_token import ApiTokenEndpoint
from .api import ApiTokenEndpoint
from .integration import (
WorkspaceIntegrationViewSet,
@@ -140,9 +132,10 @@ from .importer import (
from .page import (
PageViewSet,
PageBlockViewSet,
PageFavoriteViewSet,
CreateIssueFromPageBlockEndpoint,
PageLogEndpoint,
SubPagesEndpoint,
CreateIssueFromBlockEndpoint,
)
from .search import GlobalSearchEndpoint, IssueSearchEndpoint
@@ -155,7 +148,7 @@ from .estimate import (
BulkEstimatePointEndpoint,
)
from .inbox import InboxViewSet, InboxIssueViewSet, InboxIssuePublicViewSet
from .inbox import InboxViewSet, InboxIssueViewSet
from .analytic import (
AnalyticsEndpoint,
@@ -165,8 +158,14 @@ from .analytic import (
DefaultAnalyticsEndpoint,
)
from .notification import NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet
from .notification import (
NotificationViewSet,
UnreadNotificationEndpoint,
MarkAllReadNotificationViewSet,
)
from .exporter import ExportIssuesEndpoint
from .config import ConfigurationEndpoint
from .config import ConfigurationEndpoint
from .webhook import WebhookEndpoint, WebhookLogsEndpoint, WebhookSecretRegenerateEndpoint
@@ -5,13 +5,12 @@ from django.db.models.functions import ExtractMonth
# Third party imports
from rest_framework import status
from rest_framework.response import Response
from sentry_sdk import capture_exception
# Module imports
from plane.api.views import BaseAPIView, BaseViewSet
from plane.api.permissions import WorkSpaceAdminPermission
from plane.app.views import BaseAPIView, BaseViewSet
from plane.app.permissions import WorkSpaceAdminPermission
from plane.db.models import Issue, AnalyticView, Workspace, State, Label
from plane.api.serializers import AnalyticViewSerializer
from plane.app.serializers import AnalyticViewSerializer
from plane.utils.analytics_plot import build_graph_plot
from plane.bgtasks.analytic_plot_export import analytic_export_task
from plane.utils.issue_filters import issue_filters
+78
View File
@@ -0,0 +1,78 @@
# Python import
from uuid import uuid4
# Third party
from rest_framework.response import Response
from rest_framework import status
# Module import
from .base import BaseAPIView
from plane.db.models import APIToken, Workspace
from plane.app.serializers import APITokenSerializer, APITokenReadSerializer
from plane.app.permissions import WorkspaceOwnerPermission
class ApiTokenEndpoint(BaseAPIView):
permission_classes = [
WorkspaceOwnerPermission,
]
def post(self, request, slug):
label = request.data.get("label", str(uuid4().hex))
description = request.data.get("description", "")
workspace = Workspace.objects.get(slug=slug)
expired_at = request.data.get("expired_at", None)
# Check the user type
user_type = 1 if request.user.is_bot else 0
api_token = APIToken.objects.create(
label=label,
description=description,
user=request.user,
workspace=workspace,
user_type=user_type,
expired_at=expired_at,
)
serializer = APITokenSerializer(api_token)
# Token will be only visible while creating
return Response(
serializer.data,
status=status.HTTP_201_CREATED,
)
def get(self, request, slug, pk=None):
if pk == None:
api_tokens = APIToken.objects.filter(
user=request.user, workspace__slug=slug
)
serializer = APITokenReadSerializer(api_tokens, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
else:
api_tokens = APIToken.objects.get(
user=request.user, workspace__slug=slug, pk=pk
)
serializer = APITokenReadSerializer(api_tokens)
return Response(serializer.data, status=status.HTTP_200_OK)
def delete(self, request, slug, pk):
api_token = APIToken.objects.get(
workspace__slug=slug,
user=request.user,
pk=pk,
)
api_token.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
def patch(self, request, slug, pk):
api_token = APIToken.objects.get(
workspace__slug=slug,
user=request.user,
pk=pk,
)
serializer = APITokenSerializer(api_token, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -2,12 +2,11 @@
from rest_framework import status
from rest_framework.response import Response
from rest_framework.parsers import MultiPartParser, FormParser
from sentry_sdk import capture_exception
from django.conf import settings
# Module imports
from .base import BaseAPIView
from plane.db.models import FileAsset, Workspace
from plane.api.serializers import FileAssetSerializer
from plane.app.serializers import FileAssetSerializer
class FileAssetEndpoint(BaseAPIView):
@@ -21,7 +21,7 @@ from sentry_sdk import capture_exception
## Module imports
from . import BaseAPIView
from plane.api.serializers import (
from plane.app.serializers import (
ChangePasswordSerializer,
ResetPasswordSerializer,
)
@@ -33,7 +33,7 @@ from plane.bgtasks.forgot_password_task import forgot_password
class RequestEmailVerificationEndpoint(BaseAPIView):
def get(self, request):
token = RefreshToken.for_user(request.user).access_token
current_site = settings.WEB_URL
current_site = request.META.get('HTTP_ORIGIN')
email_verification.delay(
request.user.first_name, request.user.email, token, current_site
)
@@ -55,11 +55,11 @@ class VerifyEmailEndpoint(BaseAPIView):
return Response(
{"email": "Successfully activated"}, status=status.HTTP_200_OK
)
except jwt.ExpiredSignatureError as indentifier:
except jwt.ExpiredSignatureError as _indentifier:
return Response(
{"email": "Activation expired"}, status=status.HTTP_400_BAD_REQUEST
)
except jwt.exceptions.DecodeError as indentifier:
except jwt.exceptions.DecodeError as _indentifier:
return Response(
{"email": "Invalid token"}, status=status.HTTP_400_BAD_REQUEST
)
@@ -76,7 +76,7 @@ class ForgotPasswordEndpoint(BaseAPIView):
uidb64 = urlsafe_base64_encode(smart_bytes(user.id))
token = PasswordResetTokenGenerator().make_token(user)
current_site = settings.WEB_URL
current_site = request.META.get('HTTP_ORIGIN')
forgot_password.delay(
user.first_name, user.email, uidb64, token, current_site
+591
View File
@@ -0,0 +1,591 @@
# Python imports
import uuid
import random
import string
import json
import requests
from requests.exceptions import RequestException
# Django imports
from django.utils import timezone
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
from django.conf import settings
from django.contrib.auth.hashers import make_password
# Third party imports
from rest_framework.response import Response
from rest_framework.permissions import AllowAny
from rest_framework import status
from rest_framework_simplejwt.tokens import RefreshToken
from sentry_sdk import capture_exception, capture_message
# Module imports
from . import BaseAPIView
from plane.db.models import (
User,
WorkspaceMemberInvite,
WorkspaceMember,
ProjectMemberInvite,
ProjectMember,
)
from plane.settings.redis import redis_instance
from plane.bgtasks.magic_link_code_task import magic_link
def get_tokens_for_user(user):
refresh = RefreshToken.for_user(user)
return (
str(refresh.access_token),
str(refresh),
)
class SignUpEndpoint(BaseAPIView):
permission_classes = (AllowAny,)
def post(self, request):
if not settings.ENABLE_SIGNUP:
return Response(
{
"error": "New account creation is disabled. Please contact your site administrator"
},
status=status.HTTP_400_BAD_REQUEST,
)
email = request.data.get("email", False)
password = request.data.get("password", False)
## Raise exception if any of the above are missing
if not email or not password:
return Response(
{"error": "Both email and password are required"},
status=status.HTTP_400_BAD_REQUEST,
)
email = email.strip().lower()
try:
validate_email(email)
except ValidationError as e:
return Response(
{"error": "Please provide a valid email address."},
status=status.HTTP_400_BAD_REQUEST,
)
# Check if the user already exists
if User.objects.filter(email=email).exists():
return Response(
{"error": "User with this email already exists"},
status=status.HTTP_400_BAD_REQUEST,
)
user = User.objects.create(email=email, username=uuid.uuid4().hex)
user.set_password(password)
# settings last actives for the user
user.last_active = timezone.now()
user.last_login_time = timezone.now()
user.last_login_ip = request.META.get("REMOTE_ADDR")
user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
user.token_updated_at = timezone.now()
user.save()
# Check if user has any accepted invites for workspace and add them to workspace
workspace_member_invites = WorkspaceMemberInvite.objects.filter(
email=user.email, accepted=True
)
WorkspaceMember.objects.bulk_create(
[
WorkspaceMember(
workspace_id=workspace_member_invite.workspace_id,
member=user,
role=workspace_member_invite.role,
)
for workspace_member_invite in workspace_member_invites
],
ignore_conflicts=True,
)
# Check if user has any project invites
project_member_invites = ProjectMemberInvite.objects.filter(
email=user.email, accepted=True
)
# Add user to workspace
WorkspaceMember.objects.bulk_create(
[
WorkspaceMember(
workspace_id=project_member_invite.workspace_id,
role=project_member_invite.role
if project_member_invite.role in [5, 10, 15]
else 15,
member=user,
created_by_id=project_member_invite.created_by_id,
)
for project_member_invite in project_member_invites
],
ignore_conflicts=True,
)
# Now add the users to project
ProjectMember.objects.bulk_create(
[
ProjectMember(
workspace_id=project_member_invite.workspace_id,
role=project_member_invite.role
if project_member_invite.role in [5, 10, 15]
else 15,
member=user,
created_by_id=project_member_invite.created_by_id,
) for project_member_invite in project_member_invites
],
ignore_conflicts=True,
)
# Delete all the invites
workspace_member_invites.delete()
project_member_invites.delete()
try:
# Send Analytics
if settings.ANALYTICS_BASE_API:
_ = requests.post(
settings.ANALYTICS_BASE_API,
headers={
"Content-Type": "application/json",
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
},
json={
"event_id": uuid.uuid4().hex,
"event_data": {
"medium": "email",
},
"user": {"email": email, "id": str(user.id)},
"device_ctx": {
"ip": request.META.get("REMOTE_ADDR"),
"user_agent": request.META.get("HTTP_USER_AGENT"),
},
"event_type": "SIGN_UP",
},
)
except RequestException as e:
capture_exception(e)
access_token, refresh_token = get_tokens_for_user(user)
data = {
"access_token": access_token,
"refresh_token": refresh_token,
}
return Response(data, status=status.HTTP_200_OK)
class SignInEndpoint(BaseAPIView):
permission_classes = (AllowAny,)
def post(self, request):
email = request.data.get("email", False)
password = request.data.get("password", False)
## Raise exception if any of the above are missing
if not email or not password:
return Response(
{"error": "Both email and password are required"},
status=status.HTTP_400_BAD_REQUEST,
)
email = email.strip().lower()
try:
validate_email(email)
except ValidationError as e:
return Response(
{"error": "Please provide a valid email address."},
status=status.HTTP_400_BAD_REQUEST,
)
user = User.objects.filter(email=email).first()
if user is None:
return Response(
{
"error": "Sorry, we could not find a user with the provided credentials. Please try again."
},
status=status.HTTP_403_FORBIDDEN,
)
# Sign up Process
if not user.check_password(password):
return Response(
{
"error": "Sorry, we could not find a user with the provided credentials. Please try again."
},
status=status.HTTP_403_FORBIDDEN,
)
if not user.is_active:
return Response(
{
"error": "Your account has been deactivated. Please contact your site administrator."
},
status=status.HTTP_403_FORBIDDEN,
)
# settings last active for the user
user.last_active = timezone.now()
user.last_login_time = timezone.now()
user.last_login_ip = request.META.get("REMOTE_ADDR")
user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
user.token_updated_at = timezone.now()
user.save()
# Check if user has any accepted invites for workspace and add them to workspace
workspace_member_invites = WorkspaceMemberInvite.objects.filter(
email=user.email, accepted=True
)
WorkspaceMember.objects.bulk_create(
[
WorkspaceMember(
workspace_id=workspace_member_invite.workspace_id,
member=user,
role=workspace_member_invite.role,
)
for workspace_member_invite in workspace_member_invites
],
ignore_conflicts=True,
)
# Check if user has any project invites
project_member_invites = ProjectMemberInvite.objects.filter(
email=user.email, accepted=True
)
# Add user to workspace
WorkspaceMember.objects.bulk_create(
[
WorkspaceMember(
workspace_id=project_member_invite.workspace_id,
role=project_member_invite.role
if project_member_invite.role in [5, 10, 15]
else 15,
member=user,
created_by_id=project_member_invite.created_by_id,
)
for project_member_invite in project_member_invites
],
ignore_conflicts=True,
)
# Now add the users to project
ProjectMember.objects.bulk_create(
[
ProjectMember(
workspace_id=project_member_invite.workspace_id,
role=project_member_invite.role
if project_member_invite.role in [5, 10, 15]
else 15,
member=user,
created_by_id=project_member_invite.created_by_id,
) for project_member_invite in project_member_invites
],
ignore_conflicts=True,
)
# Delete all the invites
workspace_member_invites.delete()
project_member_invites.delete()
try:
# Send Analytics
if settings.ANALYTICS_BASE_API:
_ = requests.post(
settings.ANALYTICS_BASE_API,
headers={
"Content-Type": "application/json",
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
},
json={
"event_id": uuid.uuid4().hex,
"event_data": {
"medium": "email",
},
"user": {"email": email, "id": str(user.id)},
"device_ctx": {
"ip": request.META.get("REMOTE_ADDR"),
"user_agent": request.META.get("HTTP_USER_AGENT"),
},
"event_type": "SIGN_IN",
},
)
except RequestException as e:
capture_exception(e)
access_token, refresh_token = get_tokens_for_user(user)
data = {
"access_token": access_token,
"refresh_token": refresh_token,
}
return Response(data, status=status.HTTP_200_OK)
class SignOutEndpoint(BaseAPIView):
def post(self, request):
refresh_token = request.data.get("refresh_token", False)
if not refresh_token:
capture_message("No refresh token provided")
return Response(
{"error": "No refresh token provided"},
status=status.HTTP_400_BAD_REQUEST,
)
user = User.objects.get(pk=request.user.id)
user.last_logout_time = timezone.now()
user.last_logout_ip = request.META.get("REMOTE_ADDR")
user.save()
token = RefreshToken(refresh_token)
token.blacklist()
return Response({"message": "success"}, status=status.HTTP_200_OK)
class MagicSignInGenerateEndpoint(BaseAPIView):
permission_classes = [
AllowAny,
]
def post(self, request):
email = request.data.get("email", False)
if not email:
return Response(
{"error": "Please provide a valid email address"},
status=status.HTTP_400_BAD_REQUEST,
)
# Clean up
email = email.strip().lower()
validate_email(email)
## Generate a random token
token = (
"".join(random.choices(string.ascii_lowercase, k=4))
+ "-"
+ "".join(random.choices(string.ascii_lowercase, k=4))
+ "-"
+ "".join(random.choices(string.ascii_lowercase, k=4))
)
ri = redis_instance()
key = "magic_" + str(email)
# Check if the key already exists in python
if ri.exists(key):
data = json.loads(ri.get(key))
current_attempt = data["current_attempt"] + 1
if data["current_attempt"] > 2:
return Response(
{"error": "Max attempts exhausted. Please try again later."},
status=status.HTTP_400_BAD_REQUEST,
)
value = {
"current_attempt": current_attempt,
"email": email,
"token": token,
}
expiry = 600
ri.set(key, json.dumps(value), ex=expiry)
else:
value = {"current_attempt": 0, "email": email, "token": token}
expiry = 600
ri.set(key, json.dumps(value), ex=expiry)
current_site = request.META.get('HTTP_ORIGIN')
magic_link.delay(email, key, token, current_site)
return Response({"key": key}, status=status.HTTP_200_OK)
class MagicSignInEndpoint(BaseAPIView):
permission_classes = [
AllowAny,
]
def post(self, request):
user_token = request.data.get("token", "").strip()
key = request.data.get("key", False).strip().lower()
if not key or user_token == "":
return Response(
{"error": "User token and key are required"},
status=status.HTTP_400_BAD_REQUEST,
)
ri = redis_instance()
if ri.exists(key):
data = json.loads(ri.get(key))
token = data["token"]
email = data["email"]
if str(token) == str(user_token):
if User.objects.filter(email=email).exists():
user = User.objects.get(email=email)
if not user.is_active:
return Response(
{
"error": "Your account has been deactivated. Please contact your site administrator."
},
status=status.HTTP_403_FORBIDDEN,
)
try:
# Send event to Jitsu for tracking
if settings.ANALYTICS_BASE_API:
_ = requests.post(
settings.ANALYTICS_BASE_API,
headers={
"Content-Type": "application/json",
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
},
json={
"event_id": uuid.uuid4().hex,
"event_data": {
"medium": "code",
},
"user": {"email": email, "id": str(user.id)},
"device_ctx": {
"ip": request.META.get("REMOTE_ADDR"),
"user_agent": request.META.get("HTTP_USER_AGENT"),
},
"event_type": "SIGN_IN",
},
)
except RequestException as e:
capture_exception(e)
else:
user = User.objects.create(
email=email,
username=uuid.uuid4().hex,
password=make_password(uuid.uuid4().hex),
is_password_autoset=True,
)
try:
# Send event to Jitsu for tracking
if settings.ANALYTICS_BASE_API:
_ = requests.post(
settings.ANALYTICS_BASE_API,
headers={
"Content-Type": "application/json",
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
},
json={
"event_id": uuid.uuid4().hex,
"event_data": {
"medium": "code",
},
"user": {"email": email, "id": str(user.id)},
"device_ctx": {
"ip": request.META.get("REMOTE_ADDR"),
"user_agent": request.META.get("HTTP_USER_AGENT"),
},
"event_type": "SIGN_UP",
},
)
except RequestException as e:
capture_exception(e)
user.last_active = timezone.now()
user.last_login_time = timezone.now()
user.last_login_ip = request.META.get("REMOTE_ADDR")
user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
user.token_updated_at = timezone.now()
user.save()
# Check if user has any accepted invites for workspace and add them to workspace
workspace_member_invites = WorkspaceMemberInvite.objects.filter(
email=user.email, accepted=True
)
WorkspaceMember.objects.bulk_create(
[
WorkspaceMember(
workspace_id=workspace_member_invite.workspace_id,
member=user,
role=workspace_member_invite.role,
)
for workspace_member_invite in workspace_member_invites
],
ignore_conflicts=True,
)
# Check if user has any project invites
project_member_invites = ProjectMemberInvite.objects.filter(
email=user.email, accepted=True
)
# Add user to workspace
WorkspaceMember.objects.bulk_create(
[
WorkspaceMember(
workspace_id=project_member_invite.workspace_id,
role=project_member_invite.role
if project_member_invite.role in [5, 10, 15]
else 15,
member=user,
created_by_id=project_member_invite.created_by_id,
)
for project_member_invite in project_member_invites
],
ignore_conflicts=True,
)
# Now add the users to project
ProjectMember.objects.bulk_create(
[
ProjectMember(
workspace_id=project_member_invite.workspace_id,
role=project_member_invite.role
if project_member_invite.role in [5, 10, 15]
else 15,
member=user,
created_by_id=project_member_invite.created_by_id,
) for project_member_invite in project_member_invites
],
ignore_conflicts=True,
)
# Delete all the invites
workspace_member_invites.delete()
project_member_invites.delete()
access_token, refresh_token = get_tokens_for_user(user)
data = {
"access_token": access_token,
"refresh_token": refresh_token,
}
return Response(data, status=status.HTTP_200_OK)
else:
return Response(
{"error": "Your login code was incorrect. Please try again."},
status=status.HTTP_400_BAD_REQUEST,
)
else:
return Response(
{"error": "The magic code/link has expired please try again"},
status=status.HTTP_400_BAD_REQUEST,
)
+260
View File
@@ -0,0 +1,260 @@
# Python imports
import zoneinfo
import json
# Django imports
from django.urls import resolve
from django.conf import settings
from django.utils import timezone
from django.db import IntegrityError
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.serializers.json import DjangoJSONEncoder
# Third part imports
from rest_framework import status
from rest_framework import status
from rest_framework.viewsets import ModelViewSet
from rest_framework.response import Response
from rest_framework.exceptions import APIException
from rest_framework.views import APIView
from rest_framework.filters import SearchFilter
from rest_framework.permissions import IsAuthenticated
from sentry_sdk import capture_exception
from django_filters.rest_framework import DjangoFilterBackend
# Module imports
from plane.utils.paginator import BasePaginator
from plane.bgtasks.webhook_task import send_webhook
from plane.bgtasks.track_events import track_event
class TimezoneMixin:
"""
This enables timezone conversion according
to the user set timezone
"""
def initial(self, request, *args, **kwargs):
super().initial(request, *args, **kwargs)
if request.user.is_authenticated:
timezone.activate(zoneinfo.ZoneInfo(request.user.user_timezone))
else:
timezone.deactivate()
class WebhookMixin:
webhook_event = None
def initial_request(self, request, response, *args, **kwargs):
response = super().initial_request(request, response, *args, **kwargs)
if (
self.webhook_event
and self.request.method in ["POST", "PATCH", "DELETE"]
and response.status_code in [200, 201, 204]
):
send_webhook.delay(
event=self.webhook_event,
event_data=json.dumps(response.data, cls=DjangoJSONEncoder),
action=self.request.method,
slug=self.workspace_slug,
)
return response
class AnalyticsMixin:
analytic_event = None
print("did it come here or not")
def initial(self, request, *args, **kwargs):
super().initial(request, *args, **kwargs)
print("hieieie")
print(self.workspace_slug,"workspace_slug")
# if (
# self.analytic_event
# and self.request.method in ["POST", "PATCH", "DELETE"]
# and response.status_code in [200, 201, 204]
# ):
# print(self.workspace_slug,"workspace_slug")
# track_event.delay(
# event_name=self.analytic_event,
# slug=self.workspace_slug,
# project_id=self.project_id,
# user_id=self.request.user.id,
# )
# return response
class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
model = None
permission_classes = [
IsAuthenticated,
]
filter_backends = (
DjangoFilterBackend,
SearchFilter,
)
filterset_fields = []
search_fields = []
def get_queryset(self):
try:
return self.model.objects.all()
except Exception as e:
capture_exception(e)
raise APIException("Please check the view", status.HTTP_400_BAD_REQUEST)
def handle_exception(self, exc):
"""
Handle any exception that occurs, by returning an appropriate response,
or re-raising the error.
"""
try:
response = super().handle_exception(exc)
return response
except Exception as e:
if isinstance(e, IntegrityError):
return Response(
{"error": "The payload is not valid"},
status=status.HTTP_400_BAD_REQUEST,
)
if isinstance(e, ValidationError):
return Response(
{"error": "Please provide valid detail"},
status=status.HTTP_400_BAD_REQUEST,
)
if isinstance(e, ObjectDoesNotExist):
model_name = str(exc).split(" matching query does not exist.")[0]
return Response(
{"error": f"{model_name} does not exist."},
status=status.HTTP_404_NOT_FOUND,
)
if isinstance(e, KeyError):
capture_exception(e)
return Response(
{"error": f"key {e} does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
print(e) if settings.DEBUG else print("Server Error")
capture_exception(e)
return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def dispatch(self, request, *args, **kwargs):
try:
response = super().dispatch(request, *args, **kwargs)
if settings.DEBUG:
from django.db import connection
print(
f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}"
)
return response
except Exception as exc:
response = self.handle_exception(exc)
return exc
@property
def workspace_slug(self):
return self.kwargs.get("slug", None)
@property
def project_id(self):
project_id = self.kwargs.get("project_id", None)
if project_id:
return project_id
if resolve(self.request.path_info).url_name == "project":
return self.kwargs.get("pk", None)
class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
permission_classes = [
IsAuthenticated,
]
filter_backends = (
DjangoFilterBackend,
SearchFilter,
)
filterset_fields = []
search_fields = []
def filter_queryset(self, queryset):
for backend in list(self.filter_backends):
queryset = backend().filter_queryset(self.request, queryset, self)
return queryset
def handle_exception(self, exc):
"""
Handle any exception that occurs, by returning an appropriate response,
or re-raising the error.
"""
try:
response = super().handle_exception(exc)
return response
except Exception as e:
if isinstance(e, IntegrityError):
return Response(
{"error": "The payload is not valid"},
status=status.HTTP_400_BAD_REQUEST,
)
if isinstance(e, ValidationError):
return Response(
{"error": "Please provide valid detail"},
status=status.HTTP_400_BAD_REQUEST,
)
if isinstance(e, ObjectDoesNotExist):
model_name = str(exc).split(" matching query does not exist.")[0]
return Response(
{"error": f"{model_name} does not exist."},
status=status.HTTP_404_NOT_FOUND,
)
if isinstance(e, KeyError):
return Response({"error": f"key {e} does not exist"}, status=status.HTTP_400_BAD_REQUEST)
if settings.DEBUG:
print(e)
capture_exception(e)
return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def dispatch(self, request, *args, **kwargs):
try:
response = super().dispatch(request, *args, **kwargs)
if settings.DEBUG:
from django.db import connection
print(
f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}"
)
return response
except Exception as exc:
response = self.handle_exception(exc)
return exc
@property
def workspace_slug(self):
return self.kwargs.get("slug", None)
@property
def project_id(self):
return self.kwargs.get("project_id", None)
+95
View File
@@ -0,0 +1,95 @@
# Python imports
import os
# Django imports
from django.conf import settings
# Third party imports
from rest_framework.permissions import AllowAny
from rest_framework import status
from rest_framework.response import Response
# Module imports
from .base import BaseAPIView
from plane.license.models import Instance, InstanceConfiguration
from plane.license.utils.instance_value import get_configuration_value
class ConfigurationEndpoint(BaseAPIView):
permission_classes = [
AllowAny,
]
def get(self, request):
instance_configuration = InstanceConfiguration.objects.values("key", "value")
data = {}
# Authentication
data["google_client_id"] = get_configuration_value(
instance_configuration,
"GOOGLE_CLIENT_ID",
os.environ.get("GOOGLE_CLIENT_ID", None),
)
data["github_client_id"] = get_configuration_value(
instance_configuration,
"GITHUB_CLIENT_ID",
os.environ.get("GITHUB_CLIENT_ID", None),
)
data["github_app_name"] = get_configuration_value(
instance_configuration,
"GITHUB_APP_NAME",
os.environ.get("GITHUB_APP_NAME", None),
)
data["magic_login"] = (
bool(
get_configuration_value(
instance_configuration,
"EMAIL_HOST_USER",
os.environ.get("GITHUB_APP_NAME", None),
),
)
and bool(
get_configuration_value(
instance_configuration,
"EMAIL_HOST_PASSWORD",
os.environ.get("GITHUB_APP_NAME", None),
)
)
) and get_configuration_value(
instance_configuration, "ENABLE_MAGIC_LINK_LOGIN", "0"
) == "1"
data["email_password_login"] = (
get_configuration_value(
instance_configuration, "ENABLE_EMAIL_PASSWORD", "0"
)
== "1"
)
# Slack client
data["slack_client_id"] = get_configuration_value(
instance_configuration,
"SLACK_CLIENT_ID",
os.environ.get("SLACK_CLIENT_ID", None),
)
# Posthog
data["posthog_api_key"] = get_configuration_value(
instance_configuration,
"POSTHOG_API_KEY",
os.environ.get("POSTHOG_API_KEY", None),
)
data["posthog_host"] = get_configuration_value(
instance_configuration,
"POSTHOG_HOST",
os.environ.get("POSTHOG_HOST", None),
)
# Unsplash
data["has_unsplash_configured"] = bool(
get_configuration_value(
instance_configuration,
"UNSPLASH_ACCESS_KEY",
os.environ.get("UNSPLASH_ACCESS_KEY", None),
)
)
return Response(data, status=status.HTTP_200_OK)
@@ -3,7 +3,6 @@ import json
# Django imports
from django.db.models import (
OuterRef,
Func,
F,
Q,
@@ -21,18 +20,17 @@ from django.views.decorators.gzip import gzip_page
# Third party imports
from rest_framework.response import Response
from rest_framework import status
from sentry_sdk import capture_exception
# Module imports
from . import BaseViewSet, BaseAPIView
from plane.api.serializers import (
from . import BaseViewSet, BaseAPIView, WebhookMixin
from plane.app.serializers import (
CycleSerializer,
CycleIssueSerializer,
CycleFavoriteSerializer,
IssueStateSerializer,
CycleWriteSerializer,
)
from plane.api.permissions import ProjectEntityPermission
from plane.app.permissions import ProjectEntityPermission
from plane.db.models import (
User,
Cycle,
@@ -49,9 +47,10 @@ from plane.utils.issue_filters import issue_filters
from plane.utils.analytics_plot import burndown_plot
class CycleViewSet(BaseViewSet):
class CycleViewSet(WebhookMixin, BaseViewSet):
serializer_class = CycleSerializer
model = Cycle
webhook_event = "cycle"
permission_classes = [
ProjectEntityPermission,
]
@@ -177,9 +176,8 @@ class CycleViewSet(BaseViewSet):
def list(self, request, slug, project_id):
queryset = self.get_queryset()
cycle_view = request.GET.get("cycle_view", "all")
order_by = request.GET.get("order_by", "sort_order")
queryset = queryset.order_by(order_by)
queryset = queryset.order_by("-is_favorite","-created_at")
# Current Cycle
if cycle_view == "current":
@@ -480,13 +478,13 @@ class CycleViewSet(BaseViewSet):
)
)
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
# Delete the cycle
cycle.delete()
issue_activity.delay(
type="cycle.activity.deleted",
requested_data=json.dumps(
{
"cycle_id": str(pk),
"cycle_name": str(cycle.name),
"issues": [str(issue_id) for issue_id in cycle_issues],
}
),
@@ -496,13 +494,15 @@ class CycleViewSet(BaseViewSet):
current_instance=None,
epoch=int(timezone.now().timestamp()),
)
# Delete the cycle
cycle.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class CycleIssueViewSet(BaseViewSet):
class CycleIssueViewSet(WebhookMixin, BaseViewSet):
serializer_class = CycleIssueSerializer
model = CycleIssue
webhook_event = "cycle"
permission_classes = [
ProjectEntityPermission,
]
@@ -512,12 +512,6 @@ class CycleIssueViewSet(BaseViewSet):
"issue__assignees__id",
]
def perform_create(self, serializer):
serializer.save(
project_id=self.kwargs.get("project_id"),
cycle_id=self.kwargs.get("cycle_id"),
)
def get_queryset(self):
return self.filter_queryset(
super()
@@ -670,7 +664,7 @@ class CycleIssueViewSet(BaseViewSet):
type="cycle.activity.created",
requested_data=json.dumps({"cycles_list": issues}),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
issue_id=None,
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
{
@@ -712,6 +706,56 @@ class CycleIssueViewSet(BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT)
class CycleIssueGroupedEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def get(self, request, slug, project_id, cycle_id):
filters = issue_filters(request.query_params, "GET")
fields = [field for field in request.GET.get("fields", "").split(",") if field]
issues = (
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(bridge_id=F("issue_cycle__id"))
.filter(project_id=project_id)
.filter(workspace__slug=slug)
.select_related("project")
.select_related("workspace")
.select_related("state")
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
.filter(**filters)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
issues = IssueStateSerializer(issues, many=True, fields=fields if fields else None).data
issue_dict = {str(issue["id"]): issue for issue in issues}
return Response(
issue_dict,
status=status.HTTP_200_OK,
)
class CycleDateCheckEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
@@ -1,13 +1,12 @@
# Third party imports
from rest_framework.response import Response
from rest_framework import status
from sentry_sdk import capture_exception
# Module imports
from .base import BaseViewSet, BaseAPIView
from plane.api.permissions import ProjectEntityPermission
from plane.app.permissions import ProjectEntityPermission
from plane.db.models import Project, Estimate, EstimatePoint
from plane.api.serializers import (
from plane.app.serializers import (
EstimateSerializer,
EstimatePointSerializer,
EstimateReadSerializer,
@@ -1,15 +1,14 @@
# Third Party imports
from rest_framework.response import Response
from rest_framework import status
from sentry_sdk import capture_exception
# Module imports
from . import BaseAPIView
from plane.api.permissions import WorkSpaceAdminPermission
from plane.app.permissions import WorkSpaceAdminPermission
from plane.bgtasks.export_task import issue_export_task
from plane.db.models import Project, ExporterHistory, Workspace
from plane.api.serializers import ExporterHistorySerializer
from plane.app.serializers import ExporterHistorySerializer
class ExportIssuesEndpoint(BaseAPIView):
@@ -2,22 +2,21 @@
import requests
# Third party imports
import openai
from openai import OpenAI
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import AllowAny
from sentry_sdk import capture_exception
# Django imports
from django.conf import settings
# Module imports
from .base import BaseAPIView
from plane.api.permissions import ProjectEntityPermission
from plane.app.permissions import ProjectEntityPermission
from plane.db.models import Workspace, Project
from plane.api.serializers import ProjectLiteSerializer, WorkspaceLiteSerializer
from plane.app.serializers import ProjectLiteSerializer, WorkspaceLiteSerializer
from plane.utils.integrations.github import get_release_notes
from plane.license.models import InstanceConfiguration
from plane.license.utils.instance_value import get_configuration_value
class GPTIntegrationEndpoint(BaseAPIView):
permission_classes = [
@@ -25,7 +24,14 @@ class GPTIntegrationEndpoint(BaseAPIView):
]
def post(self, request, slug, project_id):
if not settings.OPENAI_API_KEY or not settings.GPT_ENGINE:
# Get the configuration value
instance_configuration = InstanceConfiguration.objects.values("key", "value")
api_key = get_configuration_value(instance_configuration, "OPENAI_API_KEY")
gpt_engine = get_configuration_value(instance_configuration, "GPT_ENGINE")
# Check the keys
if not api_key or not gpt_engine:
return Response(
{"error": "OpenAI API key and engine is required"},
status=status.HTTP_400_BAD_REQUEST,
@@ -41,12 +47,17 @@ class GPTIntegrationEndpoint(BaseAPIView):
final_text = task + "\n" + prompt
openai.api_key = settings.OPENAI_API_KEY
response = openai.ChatCompletion.create(
model=settings.GPT_ENGINE,
instance_configuration = InstanceConfiguration.objects.values("key", "value")
gpt_engine = get_configuration_value(instance_configuration, "GPT_ENGINE")
client = OpenAI(
api_key=api_key,
)
response = client.chat.completions.create(
model=gpt_engine,
messages=[{"role": "user", "content": final_text}],
temperature=0.7,
max_tokens=1024,
)
workspace = Workspace.objects.get(slug=slug)
@@ -89,4 +100,4 @@ class UnsplashEndpoint(BaseAPIView):
}
resp = requests.get(url=url, headers=headers)
return Response(resp.json(), status=status.HTTP_200_OK)
return Response(resp.json(), status=resp.status_code)
@@ -4,13 +4,12 @@ import uuid
# Third party imports
from rest_framework import status
from rest_framework.response import Response
from sentry_sdk import capture_exception
# Django imports
from django.db.models import Max, Q
# Module imports
from plane.api.views import BaseAPIView
from plane.app.views import BaseAPIView
from plane.db.models import (
WorkspaceIntegration,
Importer,
@@ -30,7 +29,7 @@ from plane.db.models import (
ModuleIssue,
Label,
)
from plane.api.serializers import (
from plane.app.serializers import (
ImporterSerializer,
IssueFlatSerializer,
ModuleSerializer,
@@ -39,6 +38,7 @@ from plane.utils.integrations.github import get_github_repo_details
from plane.utils.importers.jira import jira_project_issue_summary
from plane.bgtasks.importer_task import service_importer
from plane.utils.html_processor import strip_tags
from plane.app.permissions import WorkSpaceAdminPermission
class ServiceIssueImportSummaryEndpoint(BaseAPIView):
@@ -119,6 +119,9 @@ class ServiceIssueImportSummaryEndpoint(BaseAPIView):
class ImportServiceEndpoint(BaseAPIView):
permission_classes = [
WorkSpaceAdminPermission,
]
def post(self, request, slug, service):
project_id = request.data.get("project_id", False)
@@ -9,11 +9,10 @@ from django.core.serializers.json import DjangoJSONEncoder
# Third party imports
from rest_framework import status
from rest_framework.response import Response
from sentry_sdk import capture_exception
# Module imports
from .base import BaseViewSet
from plane.api.permissions import ProjectBasePermission, ProjectLitePermission
from plane.app.permissions import ProjectBasePermission, ProjectLitePermission
from plane.db.models import (
Inbox,
InboxIssue,
@@ -22,9 +21,8 @@ from plane.db.models import (
IssueLink,
IssueAttachment,
ProjectMember,
ProjectDeployBoard,
)
from plane.api.serializers import (
from plane.app.serializers import (
IssueSerializer,
InboxSerializer,
InboxIssueSerializer,
@@ -64,9 +62,7 @@ class InboxViewSet(BaseViewSet):
serializer.save(project_id=self.kwargs.get("project_id"))
def destroy(self, request, slug, project_id, pk):
inbox = Inbox.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
inbox = Inbox.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
# Handle default inbox delete
if inbox.is_default:
return Response(
@@ -128,9 +124,7 @@ class InboxIssueViewSet(BaseViewSet):
.values("count")
)
.annotate(
attachment_count=IssueAttachment.objects.filter(
issue=OuterRef("id")
)
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -150,7 +144,6 @@ class InboxIssueViewSet(BaseViewSet):
status=status.HTTP_200_OK,
)
def create(self, request, slug, project_id, inbox_id):
if not request.data.get("issue", {}).get("name", False):
return Response(
@@ -198,7 +191,7 @@ class InboxIssueViewSet(BaseViewSet):
issue_id=str(issue.id),
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp())
epoch=int(timezone.now().timestamp()),
)
# create an inbox issue
InboxIssue.objects.create(
@@ -216,10 +209,20 @@ class InboxIssueViewSet(BaseViewSet):
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
)
# Get the project member
project_member = ProjectMember.objects.get(workspace__slug=slug, project_id=project_id, member=request.user)
project_member = ProjectMember.objects.get(
workspace__slug=slug,
project_id=project_id,
member=request.user,
is_active=True,
)
# Only project members admins and created_by users can access this endpoint
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(request.user.id):
return Response({"error": "You cannot edit inbox issues"}, status=status.HTTP_400_BAD_REQUEST)
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(
request.user.id
):
return Response(
{"error": "You cannot edit inbox issues"},
status=status.HTTP_400_BAD_REQUEST,
)
# Get issue data
issue_data = request.data.pop("issue", False)
@@ -230,11 +233,13 @@ class InboxIssueViewSet(BaseViewSet):
)
# Only allow guests and viewers to edit name and description
if project_member.role <= 10:
# viewers and guests since only viewers and guests
# viewers and guests since only viewers and guests
issue_data = {
"name": issue_data.get("name", issue.name),
"description_html": issue_data.get("description_html", issue.description_html),
"description": issue_data.get("description", issue.description)
"description_html": issue_data.get(
"description_html", issue.description_html
),
"description": issue_data.get("description", issue.description),
}
issue_serializer = IssueCreateSerializer(
@@ -256,7 +261,7 @@ class InboxIssueViewSet(BaseViewSet):
IssueSerializer(current_instance).data,
cls=DjangoJSONEncoder,
),
epoch=int(timezone.now().timestamp())
epoch=int(timezone.now().timestamp()),
)
issue_serializer.save()
else:
@@ -307,7 +312,9 @@ class InboxIssueViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else:
return Response(InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK)
return Response(
InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK
)
def retrieve(self, request, slug, project_id, inbox_id, pk):
inbox_issue = InboxIssue.objects.get(
@@ -324,237 +331,29 @@ class InboxIssueViewSet(BaseViewSet):
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
)
# Get the project member
project_member = ProjectMember.objects.get(workspace__slug=slug, project_id=project_id, member=request.user)
project_member = ProjectMember.objects.get(
workspace__slug=slug,
project_id=project_id,
member=request.user,
is_active=True,
)
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(request.user.id):
return Response({"error": "You cannot delete inbox issue"}, status=status.HTTP_400_BAD_REQUEST)
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(
request.user.id
):
return Response(
{"error": "You cannot delete inbox issue"},
status=status.HTTP_400_BAD_REQUEST,
)
# Check the issue status
if inbox_issue.status in [-2, -1, 0, 2]:
# Delete the issue also
Issue.objects.filter(workspace__slug=slug, project_id=project_id, pk=inbox_issue.issue_id).delete()
inbox_issue.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class InboxIssuePublicViewSet(BaseViewSet):
serializer_class = InboxIssueSerializer
model = InboxIssue
filterset_fields = [
"status",
]
def get_queryset(self):
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=self.kwargs.get("slug"), project_id=self.kwargs.get("project_id"))
if project_deploy_board is not None:
return self.filter_queryset(
super()
.get_queryset()
.filter(
Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
project_id=self.kwargs.get("project_id"),
workspace__slug=self.kwargs.get("slug"),
inbox_id=self.kwargs.get("inbox_id"),
)
.select_related("issue", "workspace", "project")
)
else:
return InboxIssue.objects.none()
def list(self, request, slug, project_id, inbox_id):
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
if project_deploy_board.inbox is None:
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
filters = issue_filters(request.query_params, "GET")
issues = (
Issue.objects.filter(
issue_inbox__inbox_id=inbox_id,
workspace__slug=slug,
project_id=project_id,
)
.filter(**filters)
.annotate(bridge_id=F("issue_inbox__id"))
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels")
.order_by("issue_inbox__snoozed_till", "issue_inbox__status")
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=IssueAttachment.objects.filter(
issue=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.prefetch_related(
Prefetch(
"issue_inbox",
queryset=InboxIssue.objects.only(
"status", "duplicate_to", "snoozed_till", "source"
),
)
)
)
issues_data = IssueStateInboxSerializer(issues, many=True).data
return Response(
issues_data,
status=status.HTTP_200_OK,
)
def create(self, request, slug, project_id, inbox_id):
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
if project_deploy_board.inbox is None:
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
if not request.data.get("issue", {}).get("name", False):
return Response(
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
)
# Check for valid priority
if not request.data.get("issue", {}).get("priority", "none") in [
"low",
"medium",
"high",
"urgent",
"none",
]:
return Response(
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
)
# Create or get state
state, _ = State.objects.get_or_create(
name="Triage",
group="backlog",
description="Default state for managing all Inbox Issues",
project_id=project_id,
color="#ff7700",
)
# create an issue
issue = Issue.objects.create(
name=request.data.get("issue", {}).get("name"),
description=request.data.get("issue", {}).get("description", {}),
description_html=request.data.get("issue", {}).get(
"description_html", "<p></p>"
),
priority=request.data.get("issue", {}).get("priority", "low"),
project_id=project_id,
state=state,
)
# Create an Issue Activity
issue_activity.delay(
type="issue.activity.created",
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
actor_id=str(request.user.id),
issue_id=str(issue.id),
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp())
)
# create an inbox issue
InboxIssue.objects.create(
inbox_id=inbox_id,
project_id=project_id,
issue=issue,
source=request.data.get("source", "in-app"),
)
serializer = IssueStateInboxSerializer(issue)
return Response(serializer.data, status=status.HTTP_200_OK)
def partial_update(self, request, slug, project_id, inbox_id, pk):
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
if project_deploy_board.inbox is None:
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
inbox_issue = InboxIssue.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
)
# Get the project member
if str(inbox_issue.created_by_id) != str(request.user.id):
return Response({"error": "You cannot edit inbox issues"}, status=status.HTTP_400_BAD_REQUEST)
# Get issue data
issue_data = request.data.pop("issue", False)
issue = Issue.objects.get(
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
)
# viewers and guests since only viewers and guests
issue_data = {
"name": issue_data.get("name", issue.name),
"description_html": issue_data.get("description_html", issue.description_html),
"description": issue_data.get("description", issue.description)
}
issue_serializer = IssueCreateSerializer(
issue, data=issue_data, partial=True
)
if issue_serializer.is_valid():
current_instance = issue
# Log all the updates
requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder)
if issue is not None:
issue_activity.delay(
type="issue.activity.updated",
requested_data=requested_data,
actor_id=str(request.user.id),
issue_id=str(issue.id),
project_id=str(project_id),
current_instance=json.dumps(
IssueSerializer(current_instance).data,
cls=DjangoJSONEncoder,
),
epoch=int(timezone.now().timestamp())
)
issue_serializer.save()
return Response(issue_serializer.data, status=status.HTTP_200_OK)
return Response(issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def retrieve(self, request, slug, project_id, inbox_id, pk):
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
if project_deploy_board.inbox is None:
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
inbox_issue = InboxIssue.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
)
issue = Issue.objects.get(
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
)
serializer = IssueStateInboxSerializer(issue)
return Response(serializer.data, status=status.HTTP_200_OK)
def destroy(self, request, slug, project_id, inbox_id, pk):
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
if project_deploy_board.inbox is None:
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
inbox_issue = InboxIssue.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
)
if str(inbox_issue.created_by_id) != str(request.user.id):
return Response({"error": "You cannot delete inbox issue"}, status=status.HTTP_400_BAD_REQUEST)
workspace__slug=slug, project_id=project_id, pk=inbox_issue.issue_id
).delete()
inbox_issue.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -1,6 +1,6 @@
# Python improts
import uuid
import requests
# Django imports
from django.contrib.auth.hashers import make_password
@@ -10,7 +10,7 @@ from rest_framework import status
from sentry_sdk import capture_exception
# Module imports
from plane.api.views import BaseViewSet
from plane.app.views import BaseViewSet
from plane.db.models import (
Integration,
WorkspaceIntegration,
@@ -19,13 +19,13 @@ from plane.db.models import (
WorkspaceMember,
APIToken,
)
from plane.api.serializers import IntegrationSerializer, WorkspaceIntegrationSerializer
from plane.app.serializers import IntegrationSerializer, WorkspaceIntegrationSerializer
from plane.utils.integrations.github import (
get_github_metadata,
delete_github_installation,
)
from plane.api.permissions import WorkSpaceAdminPermission
from plane.app.permissions import WorkSpaceAdminPermission
from plane.utils.integrations.slack import slack_oauth
class IntegrationViewSet(BaseViewSet):
serializer_class = IntegrationSerializer
@@ -98,12 +98,19 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
config = {"installation_id": installation_id}
if provider == "slack":
metadata = request.data.get("metadata", {})
code = request.data.get("code", False)
if not code:
return Response({"error": "Code is required"}, status=status.HTTP_400_BAD_REQUEST)
slack_response = slack_oauth(code=code)
metadata = slack_response
access_token = metadata.get("access_token", False)
team_id = metadata.get("team", {}).get("id", False)
if not metadata or not access_token or not team_id:
return Response(
{"error": "Access token and team id is required"},
{"error": "Slack could not be installed. Please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
config = {"team_id": team_id, "access_token": access_token}
@@ -4,7 +4,7 @@ from rest_framework.response import Response
from sentry_sdk import capture_exception
# Module imports
from plane.api.views import BaseViewSet, BaseAPIView
from plane.app.views import BaseViewSet, BaseAPIView
from plane.db.models import (
GithubIssueSync,
GithubRepositorySync,
@@ -15,13 +15,13 @@ from plane.db.models import (
GithubCommentSync,
Project,
)
from plane.api.serializers import (
from plane.app.serializers import (
GithubIssueSyncSerializer,
GithubRepositorySyncSerializer,
GithubCommentSyncSerializer,
)
from plane.utils.integrations.github import get_github_repos
from plane.api.permissions import ProjectBasePermission, ProjectEntityPermission
from plane.app.permissions import ProjectBasePermission, ProjectEntityPermission
class GithubRepositoriesEndpoint(BaseAPIView):

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