Compare commits

..
690 Commits
Author SHA1 Message Date
Vamsi KuramaandGitHub 5c632921f8 Merge pull request #1102 from makeplane/stage-release
promote: stage-release to master
2023-05-22 00:20:23 +05:30
guru_sainathandGitHub 7c9035182b Merge pull request #1105 from makeplane/hot-fix
promote: hot-fix to stage-release
2023-05-21 19:42:15 +05:30
gurusainath b5325f14aa fix: gantt build issue resolved 2023-05-21 19:38:33 +05:30
gurusainath 4238f89cec dev: gantt ui changes 2023-05-21 19:27:08 +05:30
guru_sainathandGitHub a44cddb0fc dev: dropdown overflow issue resolved in kanban (#1106) 2023-05-21 19:08:28 +05:30
guru_sainathandGitHub 83a0c8163f dev: redirection implementation on gantt blocks (#1104) 2023-05-21 18:37:26 +05:30
gurusainath c8c195eab4 Merge branch 'stage-release' of gurusainath:makeplane/plane into stage-release 2023-05-20 23:32:07 +05:30
guru_sainathandGitHub 0acee1fe66 Merge pull request #1103 from makeplane/develop
fix: updated discord link on readme (#1101)
2023-05-20 23:31:32 +05:30
Vihar KuramaandGitHub e5f6be54e0 fix: updated discord link on readme (#1101) 2023-05-20 23:30:05 +05:30
guru_sainathandGitHub 15a846ce06 Merge pull request #1100 from makeplane/hot-fix
promote: hotfix to stage-release
2023-05-20 23:28:29 +05:30
guru_sainathandGitHub a01e241523 styles: UI changes in the gantt blocks (#1099) 2023-05-20 23:17:44 +05:30
pablohashescobarandGitHub cba62f07c0 chore: analytic export mail (#1098) 2023-05-20 23:16:19 +05:30
gurusainath c1f8766571 styles: UI changes in the gantt blocks 2023-05-20 23:10:04 +05:30
guru_sainathandGitHub af13a1b00a Merge pull request #1097 from makeplane/fix/cycle_view
fix: cycle view & create label modal
2023-05-20 22:43:55 +05:30
anmolsinghbhatia 4439740768 fix: create label modal error message updated 2023-05-20 22:30:44 +05:30
anmolsinghbhatia 98a223e5e1 fix: cycle view board view fix , feat: on gantt view set all cycle as default tab 2023-05-20 22:24:05 +05:30
guru_sainathandGitHub e081395857 Merge pull request #1095 from makeplane/develop
promote: develop to stage-release
2023-05-20 20:24:27 +05:30
gurusainath 8b527f27d0 dev: migrations for the analytics fields 2023-05-20 20:18:22 +05:30
guru_sainathandGitHub ae67dc6074 update: changed icons for gantt chat in issues, modules and cycles (#1096) 2023-05-20 20:08:17 +05:30
e1e9a5ed96 feat: Gantt chart (#1062)
* dev: Helpers

* dev: views

* dev: Chart views Month, Year and Day

* dev: Chart Workflow updates

* update: scroll functionality implementation

* update: data vaidation

* update: date renders and issue filter in the month view

* update: new date render month view

* update: scroll enabled left in chart

* update: Item render from the date it created.

* update: width implementation in chat view

* dev: chart render functionality in the gantt chart

* update: month view fix

* dev: chart render issues resolved

* update: fixed allchat views

* update: updated week view default values

* update: integrated chart view in issues

* update: grabble and sidebar logic impleemntation and integrated gantt in issues

* update: Preview gantt chart in month view

* fix: mutation in gantt chart after creating a new issue

* chore: cycles and modules list gantt chart

* update: Ui changes on gantt view

* fix: gantt chart height, chore: remove link from issue

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-05-20 17:30:15 +05:30
Aaryan KhandelwalandGitHub 9ccc35d181 fix: make custom search select handle undefined options (#1094)
* fix: make search select handle undefined options

* fix: kanban y-scroll
2023-05-20 16:00:41 +05:30
pablohashescobarandGitHub 012486df11 chore: assignee names in analytics export (#1086)
* chore: assignee names in analytics export

* chore: update key as assignee name
2023-05-20 16:00:02 +05:30
Anmol Singh BhatiaandGitHub 1fed5f7846 style: active cycle stats tab sticky (#1092) 2023-05-20 03:08:23 +05:30
Aaryan KhandelwalandGitHub 8cbe6c4b36 fix: show only filtered states when state filter is selected (#1093) 2023-05-20 03:07:00 +05:30
Dakshesh JainandGitHub d9642eee82 fix: ai response not coming when using 'AI' button (#1091)
* fix: ai response not comming for page

* fix: ai response not coming when using 'AI' button
2023-05-19 20:33:35 +05:30
Anmol Singh BhatiaandGitHub ab273f6be3 fix: sidebar dropdown fix, feat: assignee name updated (#1089)
* fix: issue sidebar cycle and module dropdown width fix

* feat: issue sidebar, issue card and issue modal assignee full name added
2023-05-19 20:00:09 +05:30
Dakshesh JainandGitHub b1f26f322f style: added theme in color picker (#1088) 2023-05-19 19:46:09 +05:30
Dakshesh JainandGitHub 2e4f936dfa fix: ai response not comming for page (#1087) 2023-05-19 19:12:34 +05:30
Dakshesh JainandGitHub ddeafc0695 fix: inconsistency for create label, select label getting closed on select (#1085) 2023-05-19 19:12:24 +05:30
Anmol Singh BhatiaandGitHub 406f02737f feat: kanban board issue menu context (#1084) 2023-05-19 19:12:15 +05:30
Dakshesh JainandGitHub 8e9afd459a fix: create issue state not clearing on create more (#1082) 2023-05-19 19:11:56 +05:30
Anmol Singh BhatiaandGitHub 186b5b5500 feat: cycle gantt view option added (#1083) 2023-05-19 18:08:47 +05:30
Aaryan KhandelwalandGitHub f2c8bdba34 fix: assignee name in analytics tooltip (#1081) 2023-05-19 17:08:32 +05:30
pablohashescobarandGitHub e162c88f03 dev: project emoji and icons (#1076) 2023-05-19 16:46:56 +05:30
Aaryan KhandelwalandGitHub 7f5fdb9589 fix: icon picker not working (#1080)
* fix: icon picker not working

* fix: project icon in analytics sidebar
2023-05-19 16:35:51 +05:30
Anmol Singh BhatiaandGitHub e3a114cd69 fix: module and cycle create issue in calendar view mutation fix (#1079) 2023-05-19 16:15:06 +05:30
Anmol Singh BhatiaandGitHub ae1eb9527a style: ui improvement (#1077)
* style: assignee, sub-issue and due date display properties styling

* style: cycle view  indicator added
2023-05-19 15:22:09 +05:30
Dakshesh JainandGitHub 6da4247400 fix: Application Error on issues list page (#1064)
* fix: Application Error on issues list page

* fix: can't read property of undefined at renderTick
2023-05-19 15:13:55 +05:30
Dakshesh JainandGitHub 4ce5a450d9 fix: pages 'I'm feeling lucky' not working (#1058) 2023-05-19 15:13:33 +05:30
pablohashescobarandGitHub 06618006c2 fix: cycle date check endpoint (#1074) 2023-05-19 13:00:38 +05:30
pablohashescobarandGitHub bb79c9de96 chore: start date for issues (#1075) 2023-05-19 12:36:12 +05:30
Aaryan KhandelwalandGitHub 09cffd5498 fix: analytics colors when segmented by cycle, module, dates, and assignees (#1072)
* fix: show unique colors when segmented

* chore: modify random color generator function

* chore: modify random color generator function
2023-05-18 19:07:24 +05:30
Anmol Singh BhatiaandGitHub 5916d6e749 fix: bug and ui fix (#1073)
* fix: cycle and module sidebar scroll

* style: date picker theming

* style: workspace slug spacing

* fix: app sidebar z-index

* fix: favorite cycle mutation

* fix: cycle modal on error close

* feat: cycle view context

* style: active cycle stats scroll

* fix: active cycle favorite mutation

* feat: import export banner

* feat: cycle sidebar date picker logic updated

* fix: NaN in progress percentage fix

* fix: tooltip fix

* style: empty state for active cycle

* style: cycle badge width fix , all cycle empty state fix and cycle icon size fix
2023-05-18 19:07:01 +05:30
pablohashescobarandGitHub c3d520aefd fix: analytics segmented export (#1068)
* fix: analytics segmented export

* dev: fix none type

* fix: analytic export y axis count
2023-05-17 18:32:10 +05:30
Aaryan KhandelwalandGitHub d41250c1ce chore: delete label confirmation modal (#1069)
* fix: negative days displayed on upcoming issues on dashboard

* chore: show completed and cancelled states by default

* chore: delete label confirmation modal
2023-05-17 16:04:56 +05:30
pablohashescobarandGitHub 27626fb16f fix: default analytic estimate points and sorting for custom analytics (#1066) 2023-05-17 14:58:45 +05:30
Aaryan KhandelwalandGitHub ab695a309f fix: layout padding, tabs size and page heading font sizes (#1067) 2023-05-17 14:57:58 +05:30
Aaryan KhandelwalandGitHub 3427652c22 chore: update analytics sidebar and header content, fix: trash box positioning (#1065)
* fix: labels dropdown on issue details page theming

* style: trash box styling and positioning

* chore: empty state for scope and demand analytics, show assignee name in scope graph tooltip

* chore: empty state for analytics

* chore: modify analytics sidebar and header
2023-05-17 13:25:58 +05:30
559b0cc9c8 style: cycle new ui (#1052)
* style: cycles new ui

* chore: added progress bar for the high priority issues

* fix: build fix

* style: active cycle details, theming , padding and layout

* style: cycle list and card styling

* style: cycle card

* fix: tooltip text overflow

* fix: cycle mutation fix

* style: cycle list and card view improvement, chore: code refactor

* feat: view cycle button

* style: cycle list and board view improvement

* style: responsiveness added

* feat: active cycle stats component, chore: code refactor

* fix: active cycle divider fix, style: stats font color

* fix: tooltip fix

---------

Co-authored-by: kunal_17 <kunalvish17360@gmail.com>
2023-05-17 12:58:01 +05:30
pablohashescobarandGitHub c49b0d6151 fix: issue assignee multiple values (#1056)
* fix: issue assignee multiple values

* chore: return first name and last name

* dev: keys update and also send data when segmented
2023-05-17 00:46:41 +05:30
pablohashescobarandGitHub d99f85ce05 chore: analytics assignee email for default analytics (#1057) 2023-05-17 00:46:20 +05:30
pablohashescobarandGitHub e696a3741c chore: analytics data ordering (#1059) 2023-05-17 00:45:59 +05:30
Aaryan KhandelwalandGitHub d575e8ec6b chore: x-axis tick values for assignees (#1060)
* style: new custom analytics ui

* fix: x-axis assignee tick values

* chore: assignee names in the custom analytics table
2023-05-16 15:11:40 +05:30
pablohashescobarandGitHub c060f7db30 chore: user first name and last name for default analytics (#1054) 2023-05-16 10:50:26 +05:30
pablohashescobarandGitHub 84a0f6f77e chore: rename effort to estimate (#1053) 2023-05-16 10:50:14 +05:30
Aaryan KhandelwalandGitHub c6d78b5e6a style: new custom analytics ui (#1055) 2023-05-16 10:41:37 +05:30
Aaryan KhandelwalandGitHub 8c707cc544 fix: lower role user cannot update higher role user (#1048)
* fix: user role update

* chore: update project member update service type
2023-05-15 23:58:41 +05:30
pablohashescobarandGitHub abe021071c fix: workspace and project member update (#1046) 2023-05-15 19:38:37 +05:30
pablohashescobarandGitHub 8d6082183e chore: return assignee avatars when x axis in assignee_email (#1049) 2023-05-15 19:38:08 +05:30
pablohashescobarandGitHub 7b52fb885d remove: onboarding emails (#1050)
* remove: user onboarding mail

* remove: welcome emails
2023-05-15 19:37:40 +05:30
pablohashescobarandGitHub d28bc41fbd chore: project members, cycles and modules count (#1051) 2023-05-15 19:37:17 +05:30
pablohashescobarandGitHub 63075a6a0d fix: analytics export and send month and year when dimension in date (#1039)
* fix: send month and year when dimension in date

* fix: export csv email

* fix: export for segment and fix segment for date values
2023-05-15 11:37:07 +05:30
Aaryan KhandelwalandGitHub 9fdc56db0f chore: user cannot update their own role (#1041) 2023-05-15 11:35:19 +05:30
Aaryan KhandelwalandGitHub 290318603d fix: calendar view mutation (#1042) 2023-05-15 11:35:07 +05:30
Anmol Singh BhatiaandGitHub dbbd9add99 fix: ui and bug fix (#1043)
* style: calendar border added

* fix: calendar issue ellipsis position fix

* fix: help section overflow fix

* fix: module card date overflow fix

* style: page detail padding and position fix

* fix: cycle and module sidebar fix
2023-05-15 11:32:50 +05:30
Aaryan KhandelwalandGitHub 37bb183bf0 chore: update analytics x-axis, tooltip and table heading values (#1040)
* chore: global bar graph component

* chore: global pie graph component

* chore: global line graph component

* chore: removed unnecessary file

* chore: refactored global chart components to accept all props

* chore: function to convert response to chart data

* chore: global calendar graph component added

* chore: global scatter plot graph component

* feat: analytics boilerplate created

* chore: null value for segment and project

* chore: clean up file

* chore: change project query param key

* chore: export, refresh buttons, analytics table

* fix: analytics fetch key error

* chore: show only integer values in the y-axis

* chore: custom x-axis tick values and bar colors

* refactor: divide analytics page into granular components

* chore: convert analytics page to modal, save analytics modal

* fix: build error

* fix: modal overflow issues, analytics loading screen

* chore: custom tooltip, refactor: graphs folder structure

* refactor: folder structure, chore: x-axis tick values for larger data

* chore: code cleanup

* chore: remove unnecessary files

* fix: refresh analytics button on error

* feat: scope and demand analytics

* refactor: scope and demand and custom analytics folder structure

* fix: dynamic import type

* chore: minor updates

* feat: project, cycle and module level analytics

* style: project analytics modal

* fix: merge conflicts

* style: new scope and demand ui, user avatars in bar graph

* fix: default analytics types

* chore: new values when dimensioned by date

* chore: update table and tooltip content when dimensioned or segmented by dates
2023-05-15 11:22:06 +05:30
vamsi 512b8c104d dev: analytics migrations 2023-05-12 17:02:34 +05:30
Aaryan KhandelwalandGitHub bf865f399f fix: kanban board horizontal scroll (#1038)
* fix: kanban board horizontal scroll

* chore: droppable placeholder position
2023-05-12 12:41:31 +05:30
pablohashescobarandGitHub 6a78948113 fix: analytics (#1037)
* fix: most issue created by user keys

* fix: cycle and module filters for GET method
2023-05-12 12:22:42 +05:30
pablohashescobarandGitHub 6e9235e5fe fix: analytic query params (#1035)
* fix: query params for analytics

* fix: default analytics filters
2023-05-11 20:53:54 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f2a68874f1 chore(deps): bump django in /apiserver/requirements (#1036)
Bumps [django](https://github.com/django/django) from 3.2.18 to 3.2.19.
- [Commits](https://github.com/django/django/compare/3.2.18...3.2.19)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-11 20:25:08 +05:30
Anmol Singh BhatiaandGitHub 9cb3794dde fix: page block padding fix (#1034) 2023-05-11 18:41:13 +05:30
Anmol Singh BhatiaandGitHub 1329145173 feat: custom theming (#1028)
* chore: custom theme types and constants

* feat: custom theming

* feat: preferences tab added in profile settings

* feat: remove unneccessary file

* feat:theme apply on page load

* fix: theme switch dropdown fix

* feat: color picker input, theme icon added, chore: code refactor

* style: color picker icon added

* fix: mutation fix

* fix: palette sequence fix

* chore: default custom theme palette updated

* style: join project and not authorized page theming

* fix: merge conflict

* fix: build fix and preferences tab layout fix
2023-05-11 18:40:17 +05:30
Anmol Singh BhatiaandGitHub 44d49b5500 style: page detail (#1030)
* style: page detail header styling

* style: page detail ui

* style: page block, create block styling
2023-05-11 18:03:37 +05:30
Aaryan KhandelwalandGitHub 1a534a3c19 feat: plane analytics (#1029)
* chore: global bar graph component

* chore: global pie graph component

* chore: global line graph component

* chore: removed unnecessary file

* chore: refactored global chart components to accept all props

* chore: function to convert response to chart data

* chore: global calendar graph component added

* chore: global scatter plot graph component

* feat: analytics boilerplate created

* chore: null value for segment and project

* chore: clean up file

* chore: change project query param key

* chore: export, refresh buttons, analytics table

* fix: analytics fetch key error

* chore: show only integer values in the y-axis

* chore: custom x-axis tick values and bar colors

* refactor: divide analytics page into granular components

* chore: convert analytics page to modal, save analytics modal

* fix: build error

* fix: modal overflow issues, analytics loading screen

* chore: custom tooltip, refactor: graphs folder structure

* refactor: folder structure, chore: x-axis tick values for larger data

* chore: code cleanup

* chore: remove unnecessary files

* fix: refresh analytics button on error

* feat: scope and demand analytics

* refactor: scope and demand and custom analytics folder structure

* fix: dynamic import type

* chore: minor updates

* feat: project, cycle and module level analytics

* style: project analytics modal

* fix: merge conflicts
2023-05-11 17:38:46 +05:30
Aaryan KhandelwalandGitHub d7928f853d chore: global graph components (#1014)
* chore: global bar graph component

* chore: global pie graph component

* chore: global line graph component

* chore: removed unnecessary file

* chore: refactored global chart components to accept all props

* chore: global calendar graph component added

* chore: global scatter plot graph component
2023-05-11 17:11:04 +05:30
Dakshesh JainandGitHub a0553722c9 fix: settings editor state according to ai response (#1032)
* fix: placeholder and ai response not getting appended in textarea

* fix: settings editor state according to ai response
2023-05-11 17:02:20 +05:30
Anmol Singh BhatiaandGitHub 34f4580b94 style: issue due date selector width fix (#1033) 2023-05-11 17:01:58 +05:30
pablohashescobarandGitHub a1d7a4ea55 fix: created by null for bulk operations (#1026) 2023-05-11 16:58:35 +05:30
pablohashescobarandGitHub abaa65b4b7 feat: analytics (#1018)
* dev: initialize plane analytics

* dev: plane analytics endpoint

* dev: update endpoint to give data with segments as well

* dev: analytics with count and effort paramters

* feat: analytics endpoints

* feat: saved analytics

* dev: remove print logs

* dev: rename x_axis to dimension in response

* dev: remove color queries

* dev: update query for None values

* feat: analytics export

* dev: update code structure send color when state or label and fix none count

* dev: uncomment try catch block

* dev: fix segment keyerror

* dev: default analytics endpoint

* dev: fix segmented results

* dev: default analytics endpoint and colors for segment

* dev: total issues and open issues by state

* dev: segment colors

* dev: fix total issue annotate

* dev: effort segmentation

* dev: total estimates and open estimates

* fix: effort when not segmented

* dev: send avatar for default analytics
2023-05-11 16:57:03 +05:30
pablohashescobarandGitHub fb165d080e feat: estimate points data in cycles list endpoint (#1015)
* feat: estimate points data in cycles list endpoint

* dev: prefetch for assignees

* dev: update sum for estimate points
2023-05-11 16:56:48 +05:30
Dakshesh JainandGitHub 083562b24c fix: placeholder and ai response not getting appended in textarea (#1031) 2023-05-11 13:41:16 +05:30
Anmol Singh BhatiaandGitHub 88d3fa549a style: page block hover effect (#1017) 2023-05-11 02:27:27 +05:30
Anmol Singh BhatiaandGitHub c7deb00f2a feat: calendar view display properties (#1024)
* fix: calendar mutation fix, chore: calendar type added

* feat: calendar view display property added

* feat: calendar header, single date and single issue component added, chore: code refactor

* chore: partialupdateissue function updated

* fix: dropdown overflow fix, style: issue card styling

* fix: calendar weekly view row height fix

* feat: calendar issue card ellipsis added, fix: edit and delete mutation fix

* style: plus icon , add issue button padding and onhover effect fix

* style: calendar issue card

* style: weekly view height
2023-05-11 02:27:14 +05:30
4f2b106852 fix: rich text editor (#1008)
* fix: undo/redo, placeholder overlapping with text, horizontal cursor

refractor: removed a lot of state-management that was not required

* fix: forwarding ref to remirror for getting extra helper functions

* fix: icon type error

* fix: value type not supported error on page block

* style: spacing, and UX for add link

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-05-11 02:15:49 +05:30
Aaryan KhandelwalandGitHub df96d40cfa fix: views issues mutation, sidebar link highlight (#1025)
* fix: views issues mutation, sidebar link highlight

* fix: show only specific states when type filter is set

* fix: delete comment mutation

* style: bulk delete issues modal

* fix: project settings features mutation
2023-05-11 02:15:39 +05:30
vamsi 4884ecd668 dev: migrations for estimate point values 2023-05-05 19:48:38 +05:30
vamsi c16c5b1cb2 Merge branch 'develop' of https://github.com/makeplane/plane into develop 2023-05-05 19:46:45 +05:30
guru_sainathandGitHub a69593a9e8 fix: handled token expiry validation (#1016) 2023-05-05 18:01:58 +05:30
a1de3f581f fix: layout height and overflow (#1004)
* fix: kanban height issue

* dev: Layout fixes

* dev: layout changes

* fix: layout overflow settings and fixed header

* style: filters padding fixed

* fix: hide filters if none are applied

---------

Co-authored-by: gurusainath <gurusainath007@gmail.com>
2023-05-05 17:07:29 +05:30
Rhea JainandGitHub 443878994a fix: breadcrumbs and tab updated (#1007) 2023-05-05 15:46:05 +05:30
Anmol Singh BhatiaandGitHub 86cb23777e fix: bug fixes (#1000)
* fix: issue sidebar cycle and module dropdown fix

* style: my issue page

* style: date picker theming

* fix: cycle modal

* style: date picker

* fix: info icon fix

* feat: integration banner

* feat: project integration banner

* fix: module card progress bar fix

* style: integration banner

* style: workspace sidebar

* fix: cycle date checker

* fix: calendar page view dropdown
2023-05-05 15:45:53 +05:30
Aaryan KhandelwalandGitHub 93c105c495 chore: track events for estimates and importers (#1012) 2023-05-05 15:45:38 +05:30
Aaryan KhandelwalandGitHub b34cf0c471 fix: ai button not working on creating a page block (#1013)
* fix: ai button not working on creating page block

* fix: build error
2023-05-05 15:45:10 +05:30
pablohashescobarandGitHub fd96c54b43 fix: cycle date check endpoint for updation (#1006)
* fix: cycle date check endpoint for updation

* dev: update the cycle date check endpoint to exclude current cycle when updating
2023-05-05 15:13:22 +05:30
pablohashescobarandGitHub 993cf3faba chore: add assignee avatar in cycle endpoint (#996)
* chore: add assignee avatar in cycle endpoint

* dev: update the structure to return avatar and firstname

* dev: return distinct users

* dev: update the structure to return id

* dev: update the prefetch queryset to distinct by id

* dev: remove id from distinct

* dev: add unique condition
2023-05-05 15:13:03 +05:30
pablohashescobarandGitHub 1bf1b63fff fix: estimate points update (#1003)
* fix: estimate points hub

* fix: estimate points update

* fix: estimate points bulk_update
2023-05-05 15:12:38 +05:30
pablohashescobarandGitHub 336220bd98 feat: return workspace and project details in estimate endpoints (#1009) 2023-05-05 15:12:22 +05:30
pablohashescobarandGitHub 5b0dc43bae docs: update readme (#1010)
docs: update readme
2023-05-05 15:12:01 +05:30
pablohashescobarandGitHub e0bec31586 feat: workspace detail for imports (#1011) 2023-05-05 15:11:45 +05:30
pablohashescobarandGitHub a2825208b8 docs: update self hosting section in readme (#1002)
docs: update self hosting section in readme
2023-05-03 23:53:53 +05:30
pablohashescobarandGitHub c3387ba974 fix: environment variables for the services (#1001) 2023-05-03 21:31:06 +05:30
pablohashescobarandGitHub baa9c30449 fix: single docker file (#999) 2023-05-03 16:48:04 +05:30
pablohashescobarandGitHub 849e2d658a dev:: update docker file for frontend (#998) 2023-05-03 16:39:13 +05:30
c7f1090914 fix: docker setup (#987)
* removing dependencies from .env

* dev: Passing the arguments from docker compose to DockerWeb in nextjs to define base environment variables

* dev: removed env from docker-compose and taking the env from shell

* dev: Updated docker file and used console in signin to test the env from docker

* dev: Docker setting env variables via shell

* removed env variables and args

* Update Dockerfile.web

* Update Dockerfile.web

* Update signin.tsx

* .

* .

* dev: Added BASE_URL from docker

* dev: Updated docker config

* dev: scripts for replacing variable during runtime

* dev: entrypoint script

* dev: update replace env script and update docker entrypoint command for frontend

* dev: update replace env script to not update process.env

* dev: update docker file to add missing variables as well

* fix: updated docker compose yml and web

* dev: create start script to run docker and update script for replacing variables

* dev: update setup script and env example script to create variables in the root of the project

* .

* dev: update docker compose hub

* dev: update docker compose hub command

* dev: update docker compose yml and env example

* dev: update docker compose hub

* dev: single docker

---------

Co-authored-by: Narayana <narayana.vadapalli1996@gmail.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>
2023-05-03 13:36:55 +05:30
Vamsi KuramaandGitHub 563bb12b9b Merge pull request #993 from makeplane/stage-release
promote: stage-release to prod
2023-05-03 01:03:31 +05:30
vamsi e3d43298df Merge branch 'stage-release' of https://github.com/makeplane/plane into stage-release 2023-05-03 00:57:04 +05:30
pablohashescobarandGitHub b36565298f Merge pull request #992 from makeplane/develop
promote: develop to stage-release
2023-05-03 00:55:14 +05:30
guru_sainathandGitHub b26e8bd956 update: UI for google auth button and loadin toggle in signin screen (#991) 2023-05-03 00:48:24 +05:30
pablohashescobarandGitHub e5703dbe70 Merge pull request #990 from makeplane/develop
promote: develop to stage-release
2023-05-02 23:37:25 +05:30
Anmol Singh BhatiaandGitHub 498d6d2b02 style: onboarding logo and style (#989) 2023-05-02 23:31:36 +05:30
pablohashescobarandGitHub 1d22817ede fix: import user invite emails (#986) 2023-05-02 20:09:54 +05:30
Anmol Singh BhatiaandGitHub 483c49d0ff fix: ui and bug fixes (#980)
* style: sub issue theming

* style: shortcut modal theming

* style: blocking and blocked by modal theming

* fix: filter labels dropdown width fix

* style: display properties

* chore: workspace invite

* style: invite co workers theming

* style: create workspace theming

* style: attachment theming

* style: workspace sidebar theming

* style: issue property theming

* style: create module modal lead icon

* style: label list modal theming

* delete attachment and member modal theming

* style: transfer issue modal

* style: delete estimate modal theming

* style: module form status

* style: delete state modal theming

* style: shortcut modal

* style: onboarding logo

* style: onboarding command menu
2023-05-02 20:00:33 +05:30
guru_sainathandGitHub 0fa9451633 fix: workspace integration github repos issue resolved (#988) 2023-05-02 18:15:24 +05:30
pablohashescobarandGitHub 46237c5431 Merge pull request #866 from makeplane/develop
promote: develop to stage-release
2023-05-02 01:57:04 +05:30
vamsi 20e400487f dev: migrations SlackProjectSync; new attributes to cycle, importer and module 2023-05-02 01:55:15 +05:30
pablohashescobarandGitHub 99fb3c9bfe chore: log cycle delete and cycle issue delete activities (#975)
* chore: log cycle delete and cycle issue delete activities

* dev: remove print logs
2023-05-02 01:21:48 +05:30
pablohashescobarandGitHub 88200a93bf feat: delete imported data as well when the import is deleted (#974) 2023-05-02 00:53:08 +05:30
pablohashescobarandGitHub b2ad071608 fix: bulk modules importer (#973) 2023-05-02 00:52:52 +05:30
pablohashescobarandGitHub 21992f540f chore: importer user invite email (#972) 2023-05-02 00:52:36 +05:30
pablohashescobarandGitHub 742731cbe6 chore: log module delete and module issue delete activities (#976) 2023-05-02 00:51:04 +05:30
pablohashescobarandGitHub c6878b9b0f fix: page access update (#977) 2023-05-02 00:50:41 +05:30
pablohashescobarandGitHub 887cac5612 feat: filter issues by estimate points (#978) 2023-05-02 00:50:10 +05:30
guru_sainathandGitHub 982566b5b4 Merge pull request #982 from makeplane/fix/slack-integration
fix: slack with project integration bug fixes
2023-05-02 00:47:36 +05:30
gurusainath 2dc5655886 fix: slack with project integration bug fixes 2023-05-02 00:40:47 +05:30
guru_sainathandGitHub 160b4a4390 Merge pull request #970 from makeplane/refactor/estimates_modal
refactor: estimates payload generate function
2023-04-29 14:17:29 +05:30
guru_sainathandGitHub f187d9512a Merge pull request #969 from makeplane/style/workspace_sidebar
style: workspace sidebar
2023-04-29 14:15:41 +05:30
Aaryan Khandelwal 53d3ea1979 refactor: estimates payload generate function 2023-04-29 03:27:03 +05:30
anmolsinghbhatia 13d76d4325 style: workspace sidebar 2023-04-28 20:51:47 +05:30
guru_sainathandGitHub 25338cc804 Merge pull request #968 from makeplane/style/onboarding_screens
style: onboarding screens, invitations page
2023-04-28 20:50:40 +05:30
Aaryan Khandelwal eaa750b025 fix: initial onboarding step 2023-04-28 20:47:29 +05:30
Aaryan Khandelwal 81d9c70026 style: onboarding screens, invitations page 2023-04-28 20:46:07 +05:30
guru_sainathandGitHub 93fb4fe1e9 Merge pull request #959 from makeplane/feat/product_update_modal
feat: product update modal
2023-04-28 19:38:33 +05:30
guru_sainathandGitHub b8e6d072cc Merge pull request #967 from makeplane/style/ui_fixes
style: command k item and cycle icon theming
2023-04-28 19:03:16 +05:30
anmolsinghbhatia 1220cebe50 style: command k item and cycle icon theming 2023-04-28 18:52:53 +05:30
anmolsinghbhatia 9464b5c00e style: product update modal 2023-04-28 18:29:01 +05:30
anmolsinghbhatia 3fae0f39c0 chore: product update interface 2023-04-28 18:28:43 +05:30
Kunal VishwakarmaandGitHub 73a757e337 fix: added the decoded url for slack integration popup (#958) 2023-04-28 17:55:57 +05:30
Kunal VishwakarmaandGitHub bb40b7feb5 chore: removed email span from invite member page (#956) 2023-04-28 17:49:38 +05:30
Aaryan KhandelwalandGitHub 3175ce9136 style: create project modal (#957) 2023-04-28 17:49:29 +05:30
Anmol Singh BhatiaandGitHub 0b9b4bb289 fix: cycle & module mutation , feat: cycle & module toast alerts (#962)
* fix: cycle remove issue mutation

* fix: module remove issue mutation

* feat: issue removed toast alert added
2023-04-28 17:49:16 +05:30
Anmol Singh BhatiaandGitHub f0f24b6fc4 style: module , cycle & icon styling (#963)
* style: target icon updated

* style: cycle card theming

* style: module card theming

* style: no current cycle message
2023-04-28 17:49:04 +05:30
Anmol Singh BhatiaandGitHub 429dffb055 fix: cycle & module sidebar default tab fix (#964) 2023-04-28 17:48:40 +05:30
guru_sainathandGitHub 6e5c85cd6e Merge pull request #961 from makeplane/style/issue_list_responsive
style: responsive design for issue list in 'list' view
2023-04-28 10:49:54 +05:30
Dakshesh Jain 2adcb163fb Merge branch 'develop' of https://github.com/makeplane/plane into style/issue_list_responsive 2023-04-25 19:44:19 +05:30
Dakshesh Jain 028a350cd1 style: issue list dropdown on next line 2023-04-25 19:43:52 +05:30
Anmol Singh BhatiaandGitHub d021a5696a fix: sub issues list fix (#960) 2023-04-25 19:00:20 +05:30
Dakshesh Jain a5c18e37c1 Merge branch 'develop' of https://github.com/makeplane/plane into style/issue_list_responsive 2023-04-25 18:36:24 +05:30
Dakshesh Jain 8e611664a8 style: made issue list view responsive 2023-04-25 18:35:58 +05:30
anmolsinghbhatia 3480b450f2 fix: build fix 2023-04-25 18:10:23 +05:30
anmolsinghbhatia a7d9591c44 feat: product updates tag and date added 2023-04-25 18:02:13 +05:30
anmolsinghbhatia 1364c842e0 feat: product updates button added 2023-04-25 17:33:53 +05:30
anmolsinghbhatia eb99b4adc9 feat: markdown to custom component 2023-04-25 16:54:12 +05:30
anmolsinghbhatia 6684dd4ab6 feat: product updates service added 2023-04-25 16:51:50 +05:30
Kunal VishwakarmaandGitHub 529ed4432c chore: fixed sidebar highlight not working (#952) 2023-04-25 12:10:05 +05:30
Kunal VishwakarmaandGitHub 8c1ad69f0c chore: changed pages empty state image (#954) 2023-04-25 12:09:29 +05:30
Kunal VishwakarmaandGitHub c23de32b03 chore: added create label inside of pages label select (#944)
* chore: added create label inside of pages label select

* chore: used footer instead of props
2023-04-25 12:08:56 +05:30
Rhea JainandGitHub 83ac1f4e4c chore: invite text update (#955) 2023-04-25 12:07:02 +05:30
Anmol Singh BhatiaandGitHub cee9695a4a feat: sub issues progress (#895) 2023-04-25 11:38:13 +05:30
Aaryan KhandelwalandGitHub c9f866e538 style: profile settings, activity theming (#951) 2023-04-24 18:53:30 +05:30
Aaryan KhandelwalandGitHub 7d96adcb70 chore: empty state for estimates (#950) 2023-04-24 18:53:18 +05:30
Aaryan KhandelwalandGitHub c5b034385f chore: added custom toggle switch everywhere (#949) 2023-04-24 18:53:07 +05:30
Aaryan KhandelwalandGitHub ff7f31c35b style: calendar view (#948)
* style: calendar view

* chore: add issue button positioning
2023-04-24 18:10:44 +05:30
Aaryan KhandelwalandGitHub 7234d6f68b style: workspace and project settings (#946) 2023-04-24 13:21:09 +05:30
Aaryan KhandelwalandGitHub d8a5b8d848 style: imports theming (#945) 2023-04-24 13:20:41 +05:30
Aaryan KhandelwalandGitHub 5412e09701 chore: empty input fields text (#943) 2023-04-24 11:28:05 +05:30
Aaryan KhandelwalandGitHub 7116acc331 style: auth pages theming (#942) 2023-04-24 11:20:14 +05:30
Aaryan KhandelwalandGitHub ae26b17cab style: filters list theming (#941) 2023-04-24 11:20:02 +05:30
Aaryan KhandelwalandGitHub 2ec8fbab34 style: modals theming (#940)
* style: existing issues list modal

* style: parent issues list modal

* style: issue modal

* style: cycle modal

* style: module modal

* style: view modal

* style: page modal

* style: delete modals
2023-04-24 11:19:53 +05:30
Aaryan KhandelwalandGitHub 213dc3f8e8 style: signin page theming (#938) 2023-04-24 11:19:43 +05:30
Aaryan KhandelwalandGitHub 4b02886c40 chore: remove outline button component (#937) 2023-04-23 01:43:58 +05:30
Aaryan KhandelwalandGitHub 8eddc4b304 fix: points not updating while editing estimate (#935) 2023-04-23 00:00:46 +05:30
Aaryan KhandelwalandGitHub 169a60723b style: project settings theming (#936)
* style: project and workspace members theming

* style: project features theming

* style: project settings states theming

* style: project settings labels theming

* style: project settings integrations theming
2023-04-22 23:46:19 +05:30
Kunal VishwakarmaandGitHub c80094581e feat: frontend slack integration (#932)
* feat: slack integration frontend

* feat: slack integration frontend complete

* Co-authored-by: Aaryan Khandelwal <aaryan610@users.noreply.github.com>
2023-04-22 21:54:50 +05:30
pablohashescobarandGitHub d99f669b89 dev: add s3 url for staging (#934) 2023-04-22 18:21:22 +05:30
Aaryan KhandelwalandGitHub 99dd1b9f0c chore: state delete validation (#930) 2023-04-22 18:19:35 +05:30
pablohashescobarandGitHub 48e77ea81b remove: state delete issue validation endpoint (#929) 2023-04-22 18:18:08 +05:30
pablohashescobarandGitHub 0be6738715 dev: back migration for integrations (#933) 2023-04-22 18:17:53 +05:30
Anmol Singh BhatiaandGitHub d041d8be6b fix: cycle date validation (#922) 2023-04-22 18:17:17 +05:30
pablohashescobarandGitHub e53847c59e feat: module view props (#882) 2023-04-22 18:16:16 +05:30
pablohashescobarandGitHub 16781a71fe feat: cycle view props (#881) 2023-04-22 18:16:05 +05:30
pablohashescobarandGitHub fb4535b294 feat: slack integration (#874)
* feat: init slack integration

* dev: create model and update existing view for slack

* dev: update slack sync model and create view to install slack

* dev: workspace integration query

* dev: update the metadata validation for access_token and team_id and save config to database

* dev: update validation for team_id

* dev: update validation

* dev: update validations

* dev: remove bot access token field from sync

* dev: handle integrity exception
2023-04-22 18:15:52 +05:30
pablohashescobarandGitHub 33a904bc3e chore: added estimate bulk endpoint for retrieving and updating (#919)
* chore: added estimate bulk endpoint for retrieving and updating

* chore: estimate endpoints

* fix: retrieve project estimate

* dev: handle integrity error check
2023-04-22 01:04:20 +05:30
pablohashescobarandGitHub 0d264838a9 remove: print exception instead use capture to log it (#926) 2023-04-22 01:04:07 +05:30
Aaryan KhandelwalandGitHub c638b6aba6 chore: new estimates workflow (#927)
* chore: new services for estimates

* chore: new estimates workflow
2023-04-22 00:59:57 +05:30
Aaryan KhandelwalandGitHub cb814dd68b chore: added new restricted workspace slugs (#928) 2023-04-22 00:57:16 +05:30
Aaryan KhandelwalandGitHub e17c824119 style: dashboard styling (#924) 2023-04-22 00:56:17 +05:30
Anmol Singh BhatiaandGitHub 68930c256f style: sidebar theming (#925)
* style: sidebar workspace dropdown theming

* style: progress chart and progress bar theming

* style: module and cycle sidebar theming
2023-04-22 00:15:45 +05:30
Anmol Singh BhatiaandGitHub e59137f6f2 style: cycle and module theming (#923)
* style: cycle theming

* style: module theming
2023-04-21 17:40:57 +05:30
Aaryan KhandelwalandGitHub 7f235739bd style: projects page theming (#921) 2023-04-21 17:40:49 +05:30
Vamsi KuramaandGitHub 20162050c3 Merge pull request #833 from kylewlacy/self-hosting-tweaks
Add a few more options for self-hosting
2023-04-21 16:47:36 +05:30
Aaryan KhandelwalandGitHub 06ad0d0f7a style: views theming (#920) 2023-04-21 16:30:16 +05:30
Aaryan KhandelwalandGitHub cfd7e1d154 fix: theming fixes (#914) 2023-04-21 15:48:06 +05:30
Anmol Singh BhatiaandGitHub fdf7ea1f82 feat: copy page link added (#915) 2023-04-21 15:46:28 +05:30
Anmol Singh BhatiaandGitHub 8f12d3d01b style: calendar theming (#918) 2023-04-21 15:45:57 +05:30
Aaryan KhandelwalandGitHub 62dc6c2f3f fix: comment box placeholder (#909) 2023-04-21 12:31:34 +05:30
Aaryan KhandelwalandGitHub 6f03022f65 chore: changed light mode colors (#913) 2023-04-21 10:46:04 +05:30
Aaryan KhandelwalandGitHub f2701a12ea style: pages theming (#912) 2023-04-21 02:15:21 +05:30
Anmol Singh BhatiaandGitHub 9129a6cde2 feat: calendar filters (#908)
* feat: hiding unnecessary filters for calendar view

* feat: filters for calendar view

* feat: module and cycle calendar view filters
2023-04-21 01:42:09 +05:30
Rhea JainandGitHub 2ba4594b29 chore: attachements button text update (#910) 2023-04-21 01:41:47 +05:30
Aaryan KhandelwalandGitHub 165d16e32b style: issue details page (#911) 2023-04-21 00:34:22 +05:30
Aaryan KhandelwalandGitHub 73388195ef chore: page block input (#907)
* fix: sidebar workspace dropdown logo

* chore: changed textarea to input for page block
2023-04-20 19:04:10 +05:30
pablohashescobarandGitHub 4dda4ec610 chore: single endpoint to create estimate and estimate points (#897) 2023-04-20 18:14:38 +05:30
pablohashescobarandGitHub e68a5382f9 feat: sub issue state distribution (#885)
* feat: sub issue state distribution

* dev: update the response structure to match consistency

* dev: update the query for sub issue distribution to include 0 issue groups as well
2023-04-20 18:14:24 +05:30
pablohashescobarandGitHub 5b6caadd6f chore: state delete validations endpoint (#880) 2023-04-20 18:14:05 +05:30
pablohashescobarandGitHub 73a8bbb31f feat: release information endpoint (#876)
* dev: init release notes

* feat: API endpoint to fetch get last 5 release information
2023-04-20 18:13:49 +05:30
Aaryan KhandelwalandGitHub 952d35dd79 style: list and kanban view theming (#906)
* fix: sidebar workspace dropdown logo

* style: list and kanban view theming
2023-04-20 18:13:21 +05:30
Anmol Singh BhatiaandGitHub 170b3d6eec fix: cycle and module sidebar fix (#905) 2023-04-20 18:09:01 +05:30
Anmol Singh BhatiaandGitHub d04a422054 feat: calendar add new issue (#901) 2023-04-20 14:11:11 +05:30
Aaryan KhandelwalandGitHub affc7655f7 fix: sidebar workspace dropdown logo (#903) 2023-04-20 13:56:09 +05:30
50c78628b3 feat: themes (#902)
* chore: add next theme and initial setup

* chore: add dark mode colors to layouts

* chore: user general setting page theming

* chore: dashboard theming

* chore: project page theming

* chore: workspace setting page theming

* chore: my issue page theming

* chore: cmdk theming

* chore: change hardcode bg and text color to theme

* chore: change color name according to the design

* style: fix card in the dashboard

* style: fix merge conflict design issues

* style: add light high contrast and dark high contrast

* style: fix cmd k menu color and selection

* feat: change theme from cmdk menu

* chore: add multiple theme field to custom theme

* chore: removed custom theming

* fix: build error

---------

Co-authored-by: Saheb Giri <iamsahebgiri@gmail.com>
2023-04-20 13:42:16 +05:30
3c2f5d12ed feat: themes (#902)
* chore: add next theme and initial setup

* chore: add dark mode colors to layouts

* chore: user general setting page theming

* chore: dashboard theming

* chore: project page theming

* chore: workspace setting page theming

* chore: my issue page theming

* chore: cmdk theming

* chore: change hardcode bg and text color to theme

* chore: change color name according to the design

* style: fix card in the dashboard

* style: fix merge conflict design issues

* style: add light high contrast and dark high contrast

* style: fix cmd k menu color and selection

* feat: change theme from cmdk menu

* chore: add multiple theme field to custom theme

* chore: removed custom theming

* fix: build error

---------

Co-authored-by: Saheb Giri <iamsahebgiri@gmail.com>
2023-04-20 13:41:24 +05:30
Rhea JainandGitHub 9f04933957 chore: upload button and github banner text update (#899)
* chore: upload button and github banner text update

* chore: attachments button text fix
2023-04-20 12:16:13 +05:30
Anmol Singh BhatiaandGitHub 8d37a3e58b feat: cycle list tab context (#900) 2023-04-20 12:11:33 +05:30
Aaryan KhandelwalandGitHub 4f61c5d552 fix: pages access mutation (#896) 2023-04-20 12:09:55 +05:30
Aaryan KhandelwalandGitHub 7149d20601 chore: change pages icon (#894) 2023-04-20 12:09:48 +05:30
Aaryan KhandelwalandGitHub d30a88832a fix: reset estimates modal form after create/update (#893) 2023-04-20 12:09:35 +05:30
Rhea JainandGitHub ebce364104 chore: changed loading text (#898) 2023-04-19 20:52:52 +05:30
Aaryan KhandelwalandGitHub c5206a7792 feat: separate filters for cycles and modules (#892) 2023-04-19 17:25:31 +05:30
Anmol Singh BhatiaandGitHub 390b837561 fix: invite button validation , style: workspace screen (#877)
* fix: invite button validation

* style: workspace screen tab height
2023-04-19 15:41:46 +05:30
Anmol Singh BhatiaandGitHub fb1932e309 fix : create issue modal (#875)
* fix: label list bug fix

* fix: assignee and label count removed

* fix: assignee and label fix
2023-04-19 15:41:17 +05:30
Dakshesh JainandGitHub ac125965eb fix: add tlds (#851) 2023-04-19 15:40:47 +05:30
Dakshesh JainandGitHub 63a36fb25d feat: jira importer (#879)
* feat: jira importer

* fix: yarn lock

* fix: displaying correct count of users that are been imported

* fix: showing workspace member in import users
2023-04-19 15:40:31 +05:30
pablohashescobarandGitHub 2b280935a1 chore: update state create endpoint to send error response on integrity error (#869)
* chore: update state create endpoint to send error response on integrity error

* dev: update status code for general exception
2023-04-18 12:25:22 +05:30
Kunal VishwakarmaandGitHub be5ef61428 fix: join project button (#873) 2023-04-18 10:55:32 +05:30
Aaryan KhandelwalandGitHub 1627a587ee refactor: workspace integrations code (#872) 2023-04-18 10:54:45 +05:30
Aaryan KhandelwalandGitHub 682a1477fb chore: create/update estimate points validation (#871) 2023-04-18 10:54:19 +05:30
Kyle Lacy ea87823478 Add $NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS to add domains for Next Image plugin 2023-04-17 16:30:36 -07:00
Kyle Lacy b8c06b3121 Add $AWS_S3_ENDPOINT_URL to .env.example 2023-04-17 14:08:47 -07:00
Kyle Lacy ca2366aa9b Use env var to set AWS_S3_ENDPOINT_URL setting 2023-04-17 14:07:00 -07:00
Aaryan KhandelwalandGitHub acff6396f9 chore: create/update state duplicate name error (#870) 2023-04-18 01:15:39 +05:30
Aaryan KhandelwalandGitHub fa5c994ddc chore: remove redundant console logs (#868) 2023-04-18 01:15:26 +05:30
Aaryan KhandelwalandGitHub 5f20e65ca6 style: list view styling reduced (#867) 2023-04-18 01:15:10 +05:30
Kyle Lacy 792162ae66 Remove redundant $REDIS_BROKER_SSL setting
A fix was already pushed upstream that made this setting unnecessary
2023-04-17 12:37:58 -07:00
Kyle Lacy e22f552ea0 Merge branch 'develop' into self-hosting-tweaks 2023-04-17 12:30:24 -07:00
pablohashescobarandGitHub 396fbc4ebb fix: redis url for docker in production settings (#865) 2023-04-17 22:38:52 +05:30
Vamsi KuramaandGitHub d2a58bf04a Merge pull request #862 from makeplane/stage-release
promote: staging to production v0.5
2023-04-17 19:08:10 +05:30
Vamsi KuramaandGitHub c51407c85e Merge branch 'master' into stage-release 2023-04-17 19:07:33 +05:30
Vamsi KuramaandGitHub 3ed937378f Merge pull request #861 from makeplane/develop
promote: patches from develop to stage-release
2023-04-17 18:48:21 +05:30
Dakshesh JainandGitHub 0fa3a8c3e3 fix: removed useEffect for project detail fetch error (#860) 2023-04-17 18:38:07 +05:30
Aaryan KhandelwalandGitHub bd0cfef02f fix: delete import (#859) 2023-04-17 18:34:18 +05:30
Dakshesh JainandGitHub 7aa9e0bba1 fix: project authorization api fetch keys (#858) 2023-04-17 18:22:04 +05:30
Vamsi KuramaandGitHub 718f62a898 Merge pull request #857 from makeplane/develop
promote: develop to stage-release
2023-04-17 17:47:19 +05:30
Kunal VishwakarmaandGitHub d26d01ace4 chore: filters spacing (#856) 2023-04-17 17:42:02 +05:30
Vamsi KuramaandGitHub 60e44fc1a2 Merge pull request #847 from makeplane/chore/my_issues_endpoint
chore: my issues endpoint to return attachment and link count
2023-04-17 17:39:38 +05:30
Anmol Singh BhatiaandGitHub 45eaa23ed0 style: dashboard content (#855) 2023-04-17 17:36:02 +05:30
Aaryan KhandelwalandGitHub 600fedd5ba fix: undefined block content (#854) 2023-04-17 17:21:15 +05:30
Aaryan KhandelwalandGitHub 6de54089cd fix: show proper issues list if no filters are present (#853) 2023-04-17 17:10:36 +05:30
Aaryan KhandelwalandGitHub dc53708109 chore: show workspace members (#852) 2023-04-17 16:41:16 +05:30
Aaryan KhandelwalandGitHub f5351e4419 fix: remove filters function (#846) 2023-04-17 15:03:56 +05:30
Aaryan KhandelwalandGitHub 819508d5fc fix: remirror placeholder (#849) 2023-04-17 15:03:44 +05:30
Kunal VishwakarmaandGitHub 0beb654069 chore: worked on making issue list padding consistent (#850) 2023-04-17 15:01:22 +05:30
Anmol Singh BhatiaandGitHub 98cef0e1e8 fix: my issue page display property (#848) 2023-04-17 14:45:51 +05:30
Aaryan KhandelwalandGitHub 8a6036a20a fix: drop to delete box zindex (#843) 2023-04-17 14:13:32 +05:30
pablohashescobar 85b6c78e75 chore: my issues endpoint to return attachment and link count 2023-04-17 14:03:19 +05:30
Anmol Singh BhatiaandGitHub 3f401b0fc5 feat: page tab context (#845)
* feat: page list tab context added

* fix: build fix
2023-04-17 13:30:39 +05:30
Anmol Singh BhatiaandGitHub 365c758a25 fix: create update view modal fix (#842) 2023-04-17 12:24:30 +05:30
Aaryan KhandelwalandGitHub ac98381f23 chore: remove estimate option from my issues (#839) 2023-04-17 12:00:30 +05:30
Kunal VishwakarmaandGitHub 3e436179fe chore: added default state for issues (#840) 2023-04-17 11:54:22 +05:30
Aaryan KhandelwalandGitHub e23075b7b9 chore: no estimates option, estimates activity (#838) 2023-04-17 11:30:48 +05:30
pablohashescobarandGitHub 61761fedc5 chore: remove view filter validation while creating or updating view (#836) 2023-04-17 11:15:38 +05:30
Kunal VishwakarmaandGitHub 5a36a7931f fix: removed extra spaces form estimate points brackets (#837) 2023-04-17 11:15:06 +05:30
Aaryan KhandelwalandGitHub 363c5c8ec4 fix: join project mutation (#835)
* fix: join project mutation

* chore: remove imports
2023-04-17 10:27:20 +05:30
Kyle Lacy 5e5d1a4699 Update .env.example with newly-added env vars 2023-04-15 12:56:43 -07:00
Kyle Lacy e2294f9105 Add EMAIL_FROM setting to change sender email for messages 2023-04-15 12:51:23 -07:00
Kyle Lacy f757d8232b Add setting to disable extra TLS config for Celery broker connection 2023-04-15 12:32:54 -07:00
Kyle Lacy 6af54ebbe7 Fix typo in `.env.example 2023-04-15 12:32:00 -07:00
Kyle Lacy 3913cf571f Make SMTP port and TLS configurable 2023-04-15 12:00:02 -07:00
Aaryan KhandelwalandGitHub 8638170a98 fix: cycles and modules sidebar mutation (#831) 2023-04-14 19:40:00 +05:30
Vamsi KuramaandGitHub ebff5d8c54 Merge pull request #830 from makeplane/develop
promote: dev to stage-release
2023-04-14 17:06:19 +05:30
vamsi b7ce69c220 dev: migrations estimate points and themes 2023-04-14 17:04:02 +05:30
Vamsi KuramaandGitHub e4da207df5 Merge pull request #768 from makeplane/feat/workspace_themes
feat: workspace themes
2023-04-14 16:56:32 +05:30
Vamsi KuramaandGitHub 3a0c5bab76 Merge pull request #804 from makeplane/fix/magic_sign_in
fix: connection error when signing in with code
2023-04-14 16:56:20 +05:30
Vamsi KuramaandGitHub 1cd1505c7d Merge pull request #805 from makeplane/chore/celery_production_settings
chore: production settings for celery
2023-04-14 16:56:00 +05:30
Vamsi KuramaandGitHub bc7fab96c3 Merge pull request #812 from makeplane/fix/parent_issue_search
fix: parent issue search
2023-04-14 16:55:20 +05:30
Vamsi KuramaandGitHub a358260a22 Merge pull request #817 from makeplane/chore/issue_estimate_points
chore: set default value as null for estimate point
2023-04-14 16:54:43 +05:30
Dakshesh JainandGitHub 3b103da6a3 chore: added unsplash flag for self-hosted (#829)
* chore: added unsplash flag for self hosted

* fix: removed actual code and only using flag

* refactor: removed extra variable
2023-04-14 16:52:31 +05:30
Aaryan KhandelwalandGitHub 23b4145565 chore: added session recorder key to env (#827) 2023-04-14 16:51:24 +05:30
Anmol Singh BhatiaandGitHub 5848c326c7 fix: cycle and module sidebar fix (#828) 2023-04-14 16:44:06 +05:30
Aaryan KhandelwalandGitHub ce253b3cc9 refactor: drag function (#826) 2023-04-14 16:41:28 +05:30
Vamsi KuramaandGitHub 3817511024 Merge pull request #824 from makeplane/feat/session_recorder
feat: session recorder
2023-04-14 16:09:31 +05:30
Kunal VishwakarmaandGitHub f50872f2a9 fix: empty issue design (#821)
* fix: empty issue design

* chore: removed unused imports
2023-04-14 16:04:16 +05:30
Anmol Singh BhatiaandGitHub a0b8f7188f fix: cycle card (#825) 2023-04-14 16:03:11 +05:30
Aaryan Khandelwal 2950877767 feat: session recorder integrated 2023-04-14 15:27:14 +05:30
Aaryan KhandelwalandGitHub c7d930f89b chore: add env flag to enable session recorder conditionally (#822) 2023-04-14 15:17:35 +05:30
pablohashescobarandGitHub 81da8715d5 chore: update bug report template to include browser environment as well (#820)
* chore: update bug report template to include browser environment as well

* chore: update type to dropdown
2023-04-14 00:45:03 +05:30
pablohashescobar b4c8323886 chore: set default value as null for estimate point 2023-04-13 19:54:34 +05:30
Anmol Singh BhatiaandGitHub c3ffd233a6 style: workspace url (#816) 2023-04-13 19:33:22 +05:30
Aaryan KhandelwalandGitHub 3fa6185b63 fix: drag and drop function (#815)
* fix: kanban drag and drop

* fix: kanban board issue dnd mutation
2023-04-13 19:09:55 +05:30
Dakshesh JainandGitHub 6de94efc7d style: removed static 'app.plane.so' (#813) 2023-04-13 18:28:23 +05:30
pablohashescobar 0cd6d9d570 fix: parent issue search 2023-04-13 17:49:09 +05:30
Aaryan KhandelwalandGitHub 657241c9c1 feat: clarity script added (#807) 2023-04-13 16:17:21 +05:30
Anmol Singh BhatiaandGitHub dc9ce5101c fix: workspace url error message (#809) 2023-04-13 15:46:25 +05:30
Kunal VishwakarmaandGitHub 3457411c6a style: issue list (#798)
* style: issue list

* chore: changed the empty state images
2023-04-13 15:39:05 +05:30
Aaryan KhandelwalandGitHub b7a7508d5d fix: workspace dashboard duplicate keys (#803) 2023-04-13 15:38:43 +05:30
Dakshesh JainandGitHub 484a88d881 fix: unusual redirection on onboarding (#808) 2023-04-13 15:38:00 +05:30
pablohashescobarandGitHub cd69b06e5e fix: error message for jira importers (#794) 2023-04-13 00:34:53 +05:30
pablohashescobarandGitHub c4609b95cd fix: remove length check condition when updating issue property (#791) 2023-04-13 00:34:37 +05:30
pablohashescobarandGitHub 6eb7ec0697 fix: typo in url for bulk creating labels (#788) 2023-04-13 00:34:23 +05:30
pablohashescobarandGitHub e232d39f0e feat: track estimate points in issue activity (#762)
* feat: track estimate points in issue activity

* dev: update comment
2023-04-13 00:34:12 +05:30
pablohashescobarandGitHub 8a26fd0a97 fix: issue link activity (#761) 2023-04-13 00:34:02 +05:30
pablohashescobarandGitHub 537a82028e fix: attachment activity (#760) 2023-04-13 00:33:50 +05:30
pablohashescobarandGitHub 440ed08728 fix: issue attachment delete (#759) 2023-04-13 00:33:37 +05:30
pablohashescobar c199e76038 chore: production settings for celery 2023-04-13 00:31:06 +05:30
pablohashescobar b40fd4bbc2 fix: connection error when signing in with code 2023-04-12 19:55:31 +05:30
Anmol Singh BhatiaandGitHub 5190ea7280 fix: send code btn fix (#802) 2023-04-12 19:03:29 +05:30
Dakshesh JainandGitHub db488338fb fix: weird redirection in index page (#801) 2023-04-12 19:03:08 +05:30
Anmol Singh BhatiaandGitHub 9196fb4562 style: onboarding screen cards (#800) 2023-04-12 19:01:46 +05:30
Saheb GiriandGitHub 34ff8fecc5 feat: add create page option in cmdk menu (#799) 2023-04-12 19:01:19 +05:30
Anmol Singh BhatiaandGitHub d6dbfdc731 feat: page improvement (#797)
* feat: remove label icon added

* feat: block menu dropdown state added

* feat: page info icon added and  style: overflow title and label fix
2023-04-12 18:07:50 +05:30
Anmol Singh BhatiaandGitHub f2e8add29d feat: attachment and link display properties (#796)
* feat: attachment and link count added

* fix: build fix
2023-04-12 15:33:30 +05:30
Saheb GiriandGitHub 032ef831b2 feat: now user can edit view (#793)
* feat: now user can edit view

* fix: build error
2023-04-12 15:33:21 +05:30
Kunal VishwakarmaandGitHub 0f9812cf2c chore: added estimate delete modal (#792) 2023-04-12 15:03:04 +05:30
Aaryan KhandelwalandGitHub f734aad10d fix: minor pages bugs (#786)
* fix: dashboard workspace activity mutation

* fix/minor_pages_bugs
2023-04-11 23:19:47 +05:30
pablohashescobarandGitHub 6279a04267 feat: attachment and link count in issues list (#777) 2023-04-11 18:47:36 +05:30
pablohashescobarandGitHub ce26bed44a fix: jira importer info endpoint to get query params (#776) 2023-04-11 18:47:18 +05:30
Anmol Singh BhatiaandGitHub 88d2adddc7 style: create issue modal icon (#784) 2023-04-11 18:46:39 +05:30
Kunal VishwakarmaandGitHub e4e66b3ae4 style: pages UI (#769)
* style: pages ui

* chore: added toast alert and tooltip

* fix: fixed issues in pages block

* fix: ai buttons inside pages block
2023-04-11 18:18:49 +05:30
Aaryan KhandelwalandGitHub f1f716e8f6 fix: estimates bugs (#785)
* fix: dashboard workspace activity mutation

* fix: minor bugs
2023-04-11 18:17:47 +05:30
Anmol Singh BhatiaandGitHub 61b9e7a161 feat: issue activity logs (#782)
* feat: attachment, link and estimate activity log added in issue detail page

* feat: attachment, link and estimate activity log added in profile activity section
2023-04-11 18:14:36 +05:30
dfa3a7b78d feat: estimates (#783)
* chore: use estimate points hook created

* chore: user auth layer

* fix: build error

* chore: estimate crud and validation

* fix: build errors

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-04-11 17:54:01 +05:30
Anmol Singh BhatiaandGitHub d5c2965946 fix: workspace joining state fix (#780) 2023-04-11 17:13:06 +05:30
Kunal VishwakarmaandGitHub cfa283116b chore: added remove workspace image in workspace settings (#781) 2023-04-11 17:12:34 +05:30
Aaryan KhandelwalandGitHub 725c9375ea fix: onboarding loop (#775)
* fix: dashboard workspace activity mutation

* fix: onboarding loop
2023-04-11 12:25:21 +05:30
Anmol Singh BhatiaandGitHub 22c1f6f8e2 feat: workspace name validation added (#767) 2023-04-11 12:11:41 +05:30
Anmol Singh BhatiaandGitHub 748e5e7fb7 fix: project setting identifier (#766)
* fix: project setting identifier validation added

* fix: project setting identifier label fix
2023-04-11 12:11:15 +05:30
Aaryan KhandelwalandGitHub 7aa0ace555 fix: pages access (#754)
* fix: dashboard workspace activity mutation

* fix: page access operation
2023-04-11 12:10:22 +05:30
Anmol Singh BhatiaandGitHub f2c5bb5c03 fix: workspace join button fix (#774) 2023-04-11 10:12:33 +05:30
pablohashescobarandGitHub 800075b781 fix: worker script (#773) 2023-04-11 09:25:33 +05:30
vamsi 6865cf4b54 chore: ssl config for REDIS connections to celery 2023-04-11 00:04:03 +05:30
pablohashescobar bc457846fe chore: move theme setting in user level from workspace level 2023-04-10 23:19:01 +05:30
Saheb GiriandGitHub d411cd7576 style: redesign view (#770)
* style: add new design to the view item

* feat: add no of filters
2023-04-10 22:46:09 +05:30
pablohashescobarandGitHub 2dbe1dd401 fix: remove migrate command from worker script (#772) 2023-04-10 22:44:11 +05:30
pablohashescobarandGitHub ec3f891b4a chore: update doc redirection url in user welcome email (#771) 2023-04-10 21:29:54 +05:30
pablohashescobar b6c911f484 feat: workspace themes 2023-04-10 18:14:09 +05:30
Anmol Singh BhatiaandGitHub fc48fb97d1 style: attachment upload button (#765) 2023-04-10 18:07:40 +05:30
Vamsi KuramaandGitHub 3618f79f89 Merge pull request #764 from makeplane/fix/setup
dev: fix setup.sh causing tr Illegal byte sequence
2023-04-10 17:45:20 +05:30
vamsi ea563d20a3 dev: fix setup.sh causing tr Illegal byte sequence 2023-04-10 17:43:19 +05:30
Vihar KuramaandGitHub 805b8f47cc Update Readme.md - Removed status section 2023-04-10 15:15:34 +05:30
Vihar KuramaandGitHub 5f216fbbbc Merge pull request #753 from makeplane/docker-hub-hotfix
Create docker-compose-hub.yml
2023-04-10 14:26:51 +05:30
Kunal VishwakarmaandGitHub 33ea1cb9d3 chore: added ai generation inside block in pages (#732) 2023-04-10 12:33:12 +05:30
Vihar KuramaandGitHub 0caadd0f7a Merge pull request #758 from nothingneko/patch-1
Fix: Dead Discord Link on Readme
2023-04-10 11:11:45 +05:30
pablohashescobarandGitHub 62e736677a fix: cycle date check endpoint (#748) 2023-04-10 10:58:32 +05:30
pablohashescobarandGitHub bd9de0c213 feat: activity for issue attachments and links (#746) 2023-04-10 10:58:22 +05:30
pablohashescobarandGitHub c80968bb23 feat: bulk update estimate endpoint (#755) 2023-04-10 10:58:09 +05:30
Jaiden RiordanandGitHub 441203867b Fix dead Discord Link 2023-04-09 18:32:08 -05:00
vamsi eac4b21ead dev: migrations for estimate Estimate, IssueAttachment 2023-04-09 03:13:46 +05:30
Narayana VadapalliandGitHub 6fe35c9fe6 Create docker-compose-hub.yml 2023-04-09 00:21:40 +05:30
Aaryan KhandelwalandGitHub 08e77cb19e fix: home page redirection logic (#752)
* fix: dashboard workspace activity mutation

* fix: home page redirection logic

* chore: add homePageRedirect function back
2023-04-08 14:32:33 -04:00
Anmol Singh BhatiaandGitHub 03e74415f2 feat: upload button validation (#747) 2023-04-08 18:20:33 +05:30
Aaryan KhandelwalandGitHub c81bc4e5d2 style: loading screens (#750)
* fix: dashboard workspace activity mutation

* style: loading screens
2023-04-08 18:20:00 +05:30
Aaryan KhandelwalandGitHub 1026ae3eb1 chore: user auth layer (#749)
* chore: use estimate points hook created

* chore: user auth layer

* fix: build error
2023-04-08 18:05:54 +05:30
Aaryan KhandelwalandGitHub 3fe32606a9 chore: disable jira importer (#745)
* fix: dashboard workspace activity mutation

* chore: remove jira importer from settings
2023-04-08 15:46:19 +05:30
pablohashescobarandGitHub d88a95b1e9 fix: celery worker for issue activities (#744)
* dev: update celery configuration to root folder

* dev: update import for celery

* fix: worker to deserialize data
2023-04-08 15:46:05 +05:30
Aaryan KhandelwalandGitHub c21fb6e942 fix: issue attachments mutation (#743)
* fix: dashboard workspace activity mutation

* fix: attachment mutation for create and delete
2023-04-08 15:33:20 +05:30
Aaryan KhandelwalandGitHub 98e6d3de22 fix: dashboard workspace activity mutation (#742) 2023-04-08 15:10:19 +05:30
Kunal VishwakarmaandGitHub 9b1ae6bcd4 fix: inconsistency in styles (#734) 2023-04-08 13:55:30 +05:30
3947a86fa7 fix: new auth layer (#740)
* chore: made workspace authorization wrapper component

* chore: added todos

* chore: workspace pages new layout

* chore: project authorization wrapper

* chore: new project authorization wrapper

* fix: authorization for member roles

* chore: new auth screens ui

---------

Co-authored-by: Dakshesh Jain <dakshesh.jain14@gmail.com>
2023-04-08 13:46:46 +05:30
Aaryan KhandelwalandGitHub beedd57ee1 fix: fetch selected project members (#741)
* fix: fetch selected project members

* chore: remove old imports
2023-04-08 13:46:21 +05:30
Kunal VishwakarmaandGitHub 0a3d13706e fix: mutation issue in cycles (#739)
* fix: mutation issue in cycles

* fix: removed comments
2023-04-08 01:24:24 +05:30
Anmol Singh BhatiaandGitHub c093209338 fix: shortcut combination key bug fix (#733)
* fix: shortcut combination key bug fix

* refactor: code refactor
2023-04-07 17:01:52 +05:30
Kunal VishwakarmaandGitHub a68d94c33f fix: added image popover for settings cover (#737)
* fix: added popover for profile cover

* fix:removed comments
2023-04-07 15:33:11 +05:30
Aaryan KhandelwalandGitHub 35f9876981 refactor: import/export code (#735) 2023-04-07 13:27:57 +05:30
pablohashescobarandGitHub c0b732f1f1 chore: rename project name to project key (#731) 2023-04-06 22:58:13 +05:30
pablohashescobarandGitHub 6be775434d fix: project issue search endpoint (#729) 2023-04-06 22:58:03 +05:30
pablohashescobarandGitHub 687b05d221 dev: upgrade python version (#728) 2023-04-06 22:57:36 +05:30
pablohashescobarandGitHub 59a33587a0 feat: delete endpoint for importers (#725)
* feat: delete endpoint for importers

* fix: delete endpoint for importers
2023-04-06 22:57:20 +05:30
pablohashescobarandGitHub e46487c130 chore: add project details on importer service endpoint (#714)
* chore: add project details on importer service endpoint

* dev: add select related for imports
2023-04-06 22:57:06 +05:30
pablohashescobarandGitHub 5b72b1672f chore: add workspace and project details on label endpoints (#713) 2023-04-06 22:56:55 +05:30
pablohashescobarandGitHub ec818a5523 refactor: move all background task from rqworker to celery (#668)
* refactor: move all background task from rqworker to celery

* dev: update background job to take input in parameters rather than a single dict

* dev: update procfile for new worker

* dev: docker updates for new celery worker
2023-04-06 22:56:36 +05:30
pablohashescobarandGitHub 100c431ac3 fix: default assignee for issues (#712)
* fix: default assignee for issues

* fix: check for empty array as well
2023-04-06 22:56:24 +05:30
Anmol Singh BhatiaandGitHub ea06ee4529 fix: issue attachment improvement (#730)
* fix: invalid file error state bug fix

* style: icons updated
2023-04-06 19:16:50 +05:30
Aaryan KhandelwalandGitHub 35455c2bf7 chore: delete import (#727)
* chore: delete import

* chore: changed button text
2023-04-06 16:06:31 +05:30
Dakshesh JainandGitHub c49f614352 feat: update project cover image (#726) 2023-04-06 15:11:01 +05:30
Kunal VishwakarmaandGitHub 95fe4a3831 feat: added estimates (#721)
* feat: added estimates

* chore: added estimate to issue sidebar
2023-04-06 15:09:24 +05:30
Anmol Singh BhatiaandGitHub 14dd498d08 feat: issue attachments feature (#717)
* chore: issue attachment services added

* feat: attachment icons added

* chore: fetch-key and icons export

* feat: issue attachment upload section added

* feat: issue attachment list section added

* feat: date helper function added

* style: responsiveness added

* feat: attachment info added

* style: attachment overflow fix

* style: cursor pointer added

* chore: issue attachment interface

* style: uploading state added

* feat: delete issue attachment modal

* style: style improvement and refactor

* style: consistent icon added , chore: refactor the code

* fix: js icon import fix

* fix: build fix

* chore: refactor code
2023-04-06 15:07:11 +05:30
pablohashescobarandGitHub 86ec46db2c dev: add imports for back migration and add migration for views (#707) 2023-04-06 13:59:43 +05:30
pablohashescobarandGitHub 6a579f85ad feat: bulk create endpoint for estimate points (#708)
* feat: bulk create endpoint for estimate points

* dev: remove integrity logic and update url
2023-04-06 13:59:17 +05:30
pablohashescobarandGitHub 105428894f fix: add check if the users need to be imported (#716) 2023-04-06 13:58:56 +05:30
pablohashescobarandGitHub f2144c3e89 fix: issue search endpoint for parent issues (#705)
* fix: issue search endpoint for parent issues

* fix: parent search

* fix: search endpoint
2023-04-06 13:58:27 +05:30
Dakshesh JainandGitHub cf662f6e6c chore: new analytic events (#699)
* feat: tracking events for issues marked as DONE, issue property update, issue moved to cycle, issue moved to modules

* fix: changed events names

* chore: sync analytic

* chore: new analytic events
2023-04-06 12:08:52 +05:30
Aaryan KhandelwalandGitHub 65037b5031 style: disabled state for buttons (#724) 2023-04-06 01:03:42 +05:30
Aaryan KhandelwalandGitHub c9d8a8dbd1 feat: github importer (#722)
* chore: github importer first step completed

* refactor: github importer code refactored

* chore: github importer functionality completed

* fix: import data step saved data
2023-04-06 00:51:15 +05:30
pablohashescobarandGitHub 6b8b981e1d docs: update readme to include default email and password for self hosting setup (#706) 2023-04-05 15:03:40 +05:30
pablohashescobarandGitHub 1562939287 chore: add workspace details on comment serializer (#697) 2023-04-05 00:20:21 +05:30
pablohashescobarandGitHub cc07e2790d feat: issue estimations (#696)
* dev: initialize estimation

* dev: issue estimation field in issues and project settings

* dev: update issue estimation logic
2023-04-05 00:19:53 +05:30
pablohashescobarandGitHub 97386e9d07 feat: issue attachments (#677) 2023-04-05 00:17:55 +05:30
pablohashescobarandGitHub ff5cddeb95 feat: issue search endpoint (#667)
* feat: issue search endpoints

* dev: update issue search for blocker and blocked by
2023-04-05 00:17:16 +05:30
Vamsi KuramaandGitHub 3d6f2dd3dc Merge pull request #703 from makeplane/stage-release
promote: stage-release to production
2023-04-04 19:38:37 +05:30
vamsi 8cbf75ad6c Merge branch 'stage-release' of https://github.com/makeplane/plane into stage-release 2023-04-04 19:33:32 +05:30
vamsi 9f4f1cac42 Merge branch 'develop' of https://github.com/makeplane/plane into stage-release 2023-04-04 19:31:12 +05:30
Vamsi KuramaandGitHub 1c752d7019 Merge pull request #691 from makeplane/develop
promote: develop to stage release
2023-04-04 19:29:55 +05:30
Aaryan KhandelwalandGitHub 3519be9ce8 fix: remirror empty state (#702)
* fix: minor pages ui

* fix: remirror empty state
2023-04-04 19:27:13 +05:30
sphynxuxandGitHub 9ce158fc10 add screenshots, feature, docker compose steps on readme (#701) 2023-04-04 18:50:23 +05:30
Aaryan KhandelwalandGitHub 0036ac6afb fix: minor pages ui (#700) 2023-04-04 18:31:28 +05:30
pablohashescobarandGitHub 7f7ceec24c chore: return user role in user onboard endpoint (#682) 2023-04-04 18:07:17 +05:30
pablohashescobarandGitHub adf366b325 chore: gpt environment variables in the example file (#698) 2023-04-04 18:07:01 +05:30
Aaryan KhandelwalandGitHub 2660d646ad chore: minor pages UI (#695)
* chore: fix minor ui bugs in pages

* chore: shortcut to add new block

* chore: keyboard accessibility

* chore: block options position
2023-04-04 16:21:46 +05:30
Aaryan KhandelwalandGitHub dad36b404d fix: pages ai modal (#694) 2023-04-04 14:07:17 +05:30
Aaryan KhandelwalandGitHub 51be70d814 chore: remove github importer tab (#693) 2023-04-04 00:27:08 +05:30
Aaryan KhandelwalandGitHub 4af5921991 chore: restrict users from creating workspace with reserved slugs (#692) 2023-04-04 00:13:21 +05:30
pablohashescobarandGitHub 588247f1c1 dev: back migration for project member views (#663) 2023-04-04 00:00:03 +05:30
pablohashescobarandGitHub 1bb93f1f50 dev: back migration to update label colors (#664) 2023-04-03 23:58:35 +05:30
pablohashescobarandGitHub d990f0038b fix: module link create url validation (#678) 2023-04-03 23:58:24 +05:30
Dakshesh JainandGitHub 67952bc225 feat: added user role on onboarding event (#684)
* feat: added tracker for views

* feat: added user role on onboarding event
2023-04-03 23:57:19 +05:30
a1f0f43992 feat: icon picker (#689)
* feat: icon picker

* style: icon picker modal

---------

Co-authored-by: Dakshesh Jain <dakshesh.jain14@gmail.com>
2023-04-03 23:54:26 +05:30
Aaryan KhandelwalandGitHub 007ed0afa4 fix: cmdk hover style (#690) 2023-04-03 23:52:39 +05:30
Kunal VishwakarmaandGitHub 62cca1c7cd feat: issue redirect (#685)
* feat: open issue in new tab for list and kanban view

* fix: used a tag
2023-04-03 23:51:46 +05:30
Aaryan KhandelwalandGitHub 5ba7d271b7 style: revamp pages (#688)
* feat: dnd added for blocks

* chore: added access option to pages

* style: ui fixes

* fix: polishing

* fix: build error
2023-04-03 23:30:29 +05:30
Saheb GiriandGitHub f6f9caf9e6 fix: now user can navigate using arrow keys (#681) 2023-04-03 19:04:11 +05:30
Aaryan KhandelwalandGitHub 35e40a7bec chore: added placeholder to remirror (#686)
* chore: added placeholder to remirror

* fix: build error
2023-04-03 18:14:50 +05:30
Aaryan KhandelwalandGitHub 3e08186d72 fix: kanban empty issues (#687) 2023-04-03 18:04:45 +05:30
Dakshesh JainandGitHub 150553d420 fix: tracker not creating user alias (#683) 2023-04-03 16:37:19 +05:30
Aaryan KhandelwalandGitHub 1b30e4b57f fix: user cannot create duplicate links (#680) 2023-04-03 16:22:37 +05:30
Anmol Singh BhatiaandGitHub a63c551e75 feat: completed cycle transfer issue validation added (#676)
* feat: completed cycle transfer issue validation added

* style: transfer issue section

* style: issue transfer button
2023-04-03 15:57:52 +05:30
Dakshesh JainandGitHub cfe7c5e0b7 fix: search button not working on image picker popover (#679) 2023-04-03 15:57:08 +05:30
Aaryan KhandelwalandGitHub 12ad3892f8 fix: signin page type (#671) 2023-04-03 15:07:39 +05:30
Dakshesh JainandGitHub 353197f583 feat: added tracker for views (#675) 2023-04-03 15:04:48 +05:30
Kunal VishwakarmaandGitHub 0cf498651c feat: shortcuts (#674)
* feat: added shortcut for page and view

* fix: shortcut for view

* fix: onclick method for views and pages
2023-04-03 14:23:50 +05:30
Saheb GiriandGitHub a18af1cecf fix: missing deleted icon in user activity (#673) 2023-04-03 11:51:25 +05:30
Aaryan KhandelwalandGitHub 61875722e4 chore: add auto generate description option to create issue modals (#672) 2023-04-03 11:41:46 +05:30
Vamsi KuramaandGitHub 7fed2ec6ef Merge pull request #670 from makeplane/stage-release
promote: stage-release to production
2023-04-01 20:29:39 +05:30
Vamsi KuramaandGitHub 93ba04aebc Merge pull request #669 from makeplane/develop
promote: develop to stage-release
2023-04-01 20:19:02 +05:30
Alejandro Pinar RuizandGitHub bd3ea456c3 fix: add tags for frontend build action (#445) 2023-04-01 18:30:26 +05:30
Saheb GiriandGitHub c0bf7783b1 style: designed user activity. (#666) 2023-03-31 23:00:28 +05:30
Anmol Singh BhatiaandGitHub 9f34f41982 feat: list and kanban view group by header icon (#665) 2023-03-31 21:20:23 +05:30
Kunal VishwakarmaandGitHub c940641ba1 Fix: view list bugs (#654)
* fix: viewlist link

* fix/removed side effects

* fix: deadzones in the link
2023-03-31 19:27:07 +05:30
Kunal VishwakarmaandGitHub 567966459b style: added icons to ellipses in project settings label (#662) 2023-03-31 18:31:44 +05:30
Anmol Singh BhatiaandGitHub 6055f5c4ee fix: cycle list page mutation fix (#661)
* fix: cycle list page mutation fix

* fix: cycle mutation fix
2023-03-31 18:31:21 +05:30
Saheb GiriandGitHub abe34ad7b1 fix: add scroll into view when editing label in settings page (#660) 2023-03-31 18:31:01 +05:30
Anmol Singh BhatiaandGitHub d596e41d4d fix: project setting control fix (#658)
* fix: project setting control fix

* fix: project member endpoint fix
2023-03-31 18:30:39 +05:30
Aaryan KhandelwalandGitHub fae1534887 fix: issues list flicker (#659)
* fix: issues list flicker

* fix: useeffect dependencies
2023-03-31 18:29:24 +05:30
Saheb GiriandGitHub 448d8c63f3 fix: old labels must have black color in project settings (#657) 2023-03-31 18:03:42 +05:30
Kunal VishwakarmaandGitHub 844ae4869a fix: view list link (#653)
* fix: viewlist link

* fix/removed side effects

* fix: deadzones in the link
2023-03-31 17:54:05 +05:30
Anmol Singh BhatiaandGitHub afd7741d0c fix: cycle status bug fix (#656) 2023-03-31 17:52:20 +05:30
Saheb GiriandGitHub f074f9f003 fix: graphs issue (#655)
* fix: prevent y axis to have decimal

* fix: add padding and labels to line chart
2023-03-31 17:33:51 +05:30
Anmol Singh BhatiaandGitHub f6500914be style: transfer issue modal empty state added (#652) 2023-03-31 16:05:02 +05:30
Saheb GiriandGitHub a6f306209d fix: issue modal title textfield reset (#651) 2023-03-31 16:04:51 +05:30
Anmol Singh BhatiaandGitHub e5507651c3 fix: issue sidebar module , cycle and label dropdown fix (#650) 2023-03-31 16:04:36 +05:30
Aaryan KhandelwalandGitHub e3005b7776 fix: project identifier check in project settings (#649) 2023-03-31 16:04:17 +05:30
Aaryan KhandelwalandGitHub 480e2c4d7f refactor: cycles toggle favorite (#648) 2023-03-31 16:03:58 +05:30
Aaryan KhandelwalandGitHub 09e17858fe chore: remove edit and push issue from page block (#647) 2023-03-31 16:03:48 +05:30
Saheb GiriandGitHub 13b2a6fd53 fix: persist data on tab switch in workspace (#646)
* fix: persist data on tab switch in workspace

* fix: build fail
2023-03-31 16:03:35 +05:30
Kunal VishwakarmaandGitHub 4ab82b9616 fix: style and bugs (#644)
* fix: style and bugs

* fix: removed unnecessary classes
2023-03-31 16:03:25 +05:30
Saheb GiriandGitHub 09d73c5e04 fix: issue activity (#645)
- fix: set and remove activity logs are showing problems
- fix: removing due date is showing static value of 1 Jan.
- fix:  old labels to have black color.
- fix:  word 'Description' should be bold
2023-03-31 14:46:17 +05:30
Vamsi KuramaandGitHub ed60707bae Merge pull request #643 from makeplane/develop
promote: develop to stage release
2023-03-31 04:41:24 +05:30
Aaryan KhandelwalandGitHub b2c15125fc fix: image upload (#642) 2023-03-31 04:24:57 +05:30
Aaryan KhandelwalandGitHub a8f125cfa8 fix: blocked and blocking in issue details sidebar (#641) 2023-03-31 03:50:35 +05:30
pablohashescobarandGitHub 15ce3537ad fix: recent pages endpoint (#640)
* fix: recent pages endpoint for date

* fix: order by
2023-03-31 03:32:32 +05:30
Aaryan KhandelwalandGitHub 448f383ec9 chore: cycle sidebar content updated (#639) 2023-03-31 03:21:37 +05:30
Aaryan KhandelwalandGitHub 0c39f0c563 style: custom error page (#638) 2023-03-31 03:09:55 +05:30
Aaryan KhandelwalandGitHub 65ddcb6d79 chore: pages content update, empty state for recent pages (#637) 2023-03-31 02:51:39 +05:30
Aaryan KhandelwalandGitHub 0c94b494ed refactor: calendar view (#636) 2023-03-31 02:43:38 +05:30
Aaryan KhandelwalandGitHub e2921539d0 refactor: sidebar stats mutation (#635) 2023-03-31 02:20:44 +05:30
Aaryan KhandelwalandGitHub 66d07e340b fix: delete mutations for issues, cycles and modules (#634) 2023-03-31 02:17:35 +05:30
vamsi 29ea592c4a dev: upgrade redis requirements to 4.5.4 2023-03-31 02:08:29 +05:30
vamsi 05323d4697 dev: new migrations for Pages related attributes 2023-03-31 02:04:33 +05:30
Anmol Singh BhatiaandGitHub 8fcfebf0fc fix: invalid date toast message updated (#633) 2023-03-31 01:40:06 +05:30
Anmol Singh BhatiaandGitHub 5d0533a44f style: ellipsis consistent style (#632)
* style: ellipsis consistent style

* style: consistent ellipsis menu for module and cycle
2023-03-31 00:31:00 +05:30
Anmol Singh BhatiaandGitHub 3cf2172520 style: delete workspace modal (#631) 2023-03-31 00:25:42 +05:30
Saheb GiriandGitHub a8a5873d88 fix: add icons to dropdown and replace link icon (#630) 2023-03-30 20:45:15 +05:30
Kunal VishwakarmaandGitHub 73c7b1bddc fix: modal typo (#629)
* fix: modal width inconsistency

* fix: modal typo
2023-03-30 20:25:04 +05:30
Anmol Singh BhatiaandGitHub 4441651f81 style: empty state image updated (#628) 2023-03-30 19:34:50 +05:30
Saheb GiriandGitHub 64c936b9b5 style: module modal consistent design (#625) 2023-03-30 19:28:04 +05:30
Dakshesh JainandGitHub 66ed6a1dc8 feat: added tracker for page, page block, and gpt (#623)
* refactor: made events function generic

* feat: added tracker for page, page block, and gpt
2023-03-30 19:27:46 +05:30
Anmol Singh BhatiaandGitHub 50275fd2ad fix: sidebar pending issue stats fix (#627) 2023-03-30 19:26:41 +05:30
Kunal VishwakarmaandGitHub 112fe8e7e6 fix: modal width inconsistency (#626) 2023-03-30 19:02:05 +05:30
Anmol Singh BhatiaandGitHub 02e6439bd5 feat: completed cycle transfer issue (#624)
* feat: bulk transfer issue for completed cycle added

* feat: toast alert added for issue transfer
2023-03-30 18:59:53 +05:30
pablohashescobarandGitHub f9ee898d88 chore: add workspace details and project details in page responses (#615)
* chore: add workspace details and project details in page responses

* fix: typo in workspace queryset
2023-03-30 18:59:39 +05:30
pablohashescobarandGitHub f79fdbf782 chore: create activity when a block is converter into an issue (#609) 2023-03-30 18:58:51 +05:30
pablohashescobarandGitHub 1d7b65ad83 fix: transfer issues endpoint (#620) 2023-03-30 18:58:23 +05:30
Saheb GiriandGitHub 1558f51c23 fix: show divider only when filter is selected (#618) 2023-03-30 18:54:44 +05:30
Aaryan KhandelwalandGitHub bc7dc43171 chore: ai assistant limit error (#619) 2023-03-30 18:45:33 +05:30
Kunal VishwakarmaandGitHub 70a00a6309 style: issue modal scroll (#621) 2023-03-30 18:45:11 +05:30
Saheb GiriandGitHub 4d56adba43 style: create cycle modal makeover (#617) 2023-03-30 18:35:29 +05:30
Anmol Singh BhatiaandGitHub 624d9dfd39 fix: icon and ellipsis (#622)
* fix: consistent icon for end date for cycle card

* fix: sidebar ellipsis fix
2023-03-30 18:35:05 +05:30
Aaryan KhandelwalandGitHub 3e8c375d1c fix: old labels color set to black (#614) 2023-03-30 18:07:18 +05:30
Aaryan KhandelwalandGitHub 880813685b fix: sidebar dropdowns height (#616)
* fix: old labels color set to black

* fix: issue details sidebar select dropdowns height
2023-03-30 18:07:00 +05:30
Aaryan KhandelwalandGitHub 16abbe0b3e fix: project identifier check in project settings (#613) 2023-03-30 17:48:03 +05:30
Aaryan KhandelwalandGitHub b65fa89cdb fix: role input on my profile page (#612) 2023-03-30 17:47:49 +05:30
Aaryan KhandelwalandGitHub ed4aae47a2 style: my profile page (#608) 2023-03-30 17:04:41 +05:30
Aaryan KhandelwalandGitHub 5feaed3961 fix: fetch correct list of issues on the calendar view (#611) 2023-03-30 17:03:37 +05:30
Saheb GiriandGitHub be5c4140ff refactor: issue label and make design consistent (#610) 2023-03-30 16:59:07 +05:30
pablohashescobarandGitHub 9c4fcca6c1 fix: ordering in queryset (#598) 2023-03-30 16:33:16 +05:30
pablohashescobarandGitHub 5aad20e7ed fix: project update for identifier (#604) 2023-03-30 16:33:04 +05:30
pablohashescobarandGitHub 3a599b6436 chore: workspace detail on issue activity serializer (#607) 2023-03-30 16:32:48 +05:30
Kunal VishwakarmaandGitHub c5ccc29418 feat: added invitaion pending pills (#606) 2023-03-30 16:01:24 +05:30
Anmol Singh BhatiaandGitHub f5f90dab69 fix : module and cycle invalid date fix (#605)
* fix: module and cycle modal invalid date validation

* fix: cycle and module sidebar invalid date
2023-03-30 16:00:48 +05:30
Aaryan KhandelwalandGitHub a94e38c093 fix: minor bugs in the onboarding screens (#603) 2023-03-30 13:55:50 +05:30
Saheb GiriandGitHub 63b7c1ee47 style: icons consistency and minor fixes (#601)
* fix: change discord casing

* style: icon and style consistency in cmd k

- change discord icon color
- make link copy link consistent
-  bolt to rocket icon for shortcut

* style: make icon color throughout the activity log
2023-03-30 13:54:18 +05:30
Kunal VishwakarmaandGitHub 1866fd77bb fix: changed buttons to primary (#602) 2023-03-30 13:53:03 +05:30
Kunal VishwakarmaandGitHub 531b9e3d64 style: fixed height issue in module card (#599) 2023-03-30 12:09:42 +05:30
Anmol Singh BhatiaandGitHub 06fb3e9b58 fix: calendar view bugs (#600)
* fix: text turncate added for issue

* fix: next week btn fix, style:calendar cell height fix
2023-03-30 12:09:11 +05:30
Aaryan KhandelwalandGitHub fb01e6d22c fix: module status line break (#597) 2023-03-30 02:07:48 +05:30
pablohashescobarandGitHub 89bb439d62 chore: return user request count from logger (#596) 2023-03-30 02:04:41 +05:30
e6055da150 feat: calendar view (#561)
* feat:start and last day of month helper function

* feat: start and last day of week helper function

* feat: weekday and everyday interval helper function

* feat: calendar date formater helper function

* feat: monthly calendar view , feat: weekend date toggle, feat: calendar month and year picker

* feat: monthly , weekly view and weekend toggle

* feat: drag and drop added in calendar view

* fix: drag and drop mutation fix

* chore: refactoring , feat: calendar view option added

* fix: calendar view menu fix

* style: calendar view style improvement

* style: drag and drop styling

* fix:week day format fix

* chore: calendar constant added

* feat: calendar helper function added

* feat: month and year picker, month navigator, jump to today funtionality added

* feat: weekly navigation and jump to today fix, style: month year picker fix

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-03-30 02:01:53 +05:30
Aaryan KhandelwalandGitHub 952c64d449 fix: mutation error for project issues (#595) 2023-03-30 01:50:00 +05:30
Aaryan KhandelwalandGitHub dcf8b562d3 chore: cycle and module sidebar stats filter (#594) 2023-03-30 01:31:43 +05:30
6a40dd911f fix: cycles date check for null and incomplete cycles endpoint (#580)
Co-authored-by: Vamsi Kurama <vamsi.kurama@gmail.com>
2023-03-30 01:26:14 +05:30
pablohashescobarandGitHub 9064709d5d feat: user activities endpoint (#590) 2023-03-30 01:25:55 +05:30
6d8eda9814 chore: update API endpoints for analytics (#574)
* chore: return workspace details on project create and update

* chore: update endpoints to return workspace and project details

---------

Co-authored-by: Vamsi Kurama <vamsi.kurama@gmail.com>
2023-03-30 01:25:32 +05:30
pablohashescobarandGitHub 7eaec16381 chore: add cycle id and module id in issues list endpoint (#586) 2023-03-30 01:25:16 +05:30
pablohashescobarandGitHub d7ed237f78 feat: add API limit for AI assistance (#592) 2023-03-30 01:23:56 +05:30
kunalv17andGitHub 5c1607f39b style: finished module card design (#593)
* style: finished modulecard design

* fix: use assignee list component
2023-03-30 01:17:51 +05:30
Saheb GiriandGitHub c75ca8203f feat: add label to create view modal (#591) 2023-03-30 01:08:14 +05:30
Anmol Singh BhatiaandGitHub cfd97041b8 feat: incomplete cycle endpoint added , issue sidebar cycle fix (#582)
* feat: incomplete cycle endpoint added , issue sidebar cycle fix

* fix: fetch key
2023-03-29 19:21:15 +05:30
Saheb GiriandGitHub 7337707a4e style: redesign issue activity (#579)
* style: redesign the activity feed and refactor the code

* chore: remove console logs

* revert: back to remirror rich text editor

* style: make icons, text smaller and reduce the spacing between logs
2023-03-29 19:19:37 +05:30
Dakshesh JainandGitHub b441a2ce20 feat: added labels in filters (#585)
* feat: added labels in filters

* fix: added labels in fetch keys
2023-03-29 19:18:57 +05:30
Aaryan KhandelwalandGitHub 1509c8611d fix: mutation while adding issue to cycle or module (#589) 2023-03-29 19:13:53 +05:30
kunalv17andGitHub 4e9715a5b2 style: added divider to the pages block (#571)
* style: added hr to the pages block

* style: added divider and removed hr

* chore: removed index prop

* chore: removed index
2023-03-29 18:36:52 +05:30
Dakshesh JainandGitHub 10657d4796 fix: filter menu not closing on select other parent option (#587) 2023-03-29 18:18:45 +05:30
Saheb GiriandGitHub 653cc29290 style: issue filter design (#588)
* style: redesign issue filter pill

* style: redesign issue label on the view modal
2023-03-29 18:18:08 +05:30
Dakshesh JainandGitHub fa9c6581fd fix: using proper name for tracker key (#581)
* fix: transmitting selective data for analytics

* fix: using proper name for tracker key

* fix: using proper name for tracker key
2023-03-29 16:34:56 +05:30
Aaryan KhandelwalandGitHub 96910e1897 chore: ai for issue description (#575)
* feat: block sync

* chore: ai assistant for issue description
2023-03-29 16:30:40 +05:30
sphynxuxandGitHub 2f69761130 Merge pull request #583 from makeplane/chore/analytics_payload
chore: analytics payload
2023-03-29 16:29:17 +05:30
sphynxuxandGitHub a8f5a3eda1 Merge pull request #584 from makeplane/fix/parent_issue_modal
fix: design of select parent issue modal
2023-03-29 16:28:55 +05:30
Anmol Singh BhatiaandGitHub 248d094762 feat: cycle list page (#577)
* style: cycle list page

* fix:typo fix
2023-03-29 16:27:55 +05:30
pablohashescobar 1255f4756d fix: uuid serilaizable error 2023-03-29 16:25:06 +05:30
Aaryan Khandelwal 22f8eb9a68 fix: design of select parent issue modal 2023-03-29 16:24:04 +05:30
pablohashescobar 3ba9cddc2a chore: add user id when logging for analytics 2023-03-29 16:15:11 +05:30
kunalv17andGitHub e3cb0ed13e feat/added toggle for pages in settings features (#576)
* feat/added toggle for pages in settings features

* style:changed pages icon
2023-03-29 15:21:08 +05:30
pablohashescobar 2786f09e85 Merge branch 'develop' of github.com:makeplane/plane into develop 2023-03-29 14:52:38 +05:30
Dakshesh JainandGitHub 06daf68753 fix: transmitting selective data for analytics (#578) 2023-03-29 14:38:30 +05:30
Anmol Singh BhatiaandGitHub 2138ddf1f5 fix: profile page header fix (#572) 2023-03-29 13:17:31 +05:30
pablohashescobar f9fa345b25 Merge branch 'develop' of github.com:makeplane/plane into develop 2023-03-29 12:54:33 +05:30
Dakshesh JainandGitHub c3e1d33518 feat: jitsu tracker setup (#542)
* feat: jitsu tracker setup

also using it in create, update and delete project & workspace

* refactor: added some more check condition on track-event api

* fix: added env vars in turbo.json

* feat: added user onboard event, workspace invite and workspace invite accept events

* feat: add tracker for issues, state, modules and cycles

* fix: add @jitsu/nextjs in package.json
2023-03-29 12:24:19 +05:30
kunalv17andGitHub 9eb9b7bf6c style: dashboard color (#568) 2023-03-29 11:54:37 +05:30
Saheb GiriandGitHub c60b152a7c fix: add crisp chat with us option in cmdk (#570) 2023-03-29 11:53:09 +05:30
Anmol Singh BhatiaandGitHub e07ffc3a46 fix: module sidebar link section relocation (#569) 2023-03-29 11:52:39 +05:30
Saheb GiriandGitHub dd3bca9a32 fix: cmdk integration (#567)
* fix: issues not showing on cmd k

* fix: text overflows on longer issue title

* fix: add loading state whenever there is a network call

* fix: minor ux changes

* feat: replace loading with spinner
2023-03-29 00:51:47 +05:30
628591854d fix: search endpoint to return distint results (#566)
Co-authored-by: Vamsi Kurama <vamsi.kurama@gmail.com>
2023-03-29 00:23:35 +05:30
pablohashescobarandGitHub 05e9c0f76f fix: cycle validation for completed cycles (#559) 2023-03-29 00:23:16 +05:30
pablohashescobarandGitHub 3bebcc4714 chore: return gpt response in html (#555)
* chore: return gpt response in html

* chore: update the response br
2023-03-29 00:23:02 +05:30
pablohashescobarandGitHub d16d32cea8 feat: move issues from one cycle to another (#554)
* feat: move issues from one cycle to another

* fix: push method when updating

* fix: new cycle completed validation
2023-03-29 00:22:48 +05:30
Aaryan KhandelwalandGitHub b654d30aeb chore: minor fixes on pages (#557)
* feat: block sync

* chore: minor fixes on pages

* fix: remove dangerously set inner html

* fix: pages crud operations mutation

* fix: favorites mutation for recent pages

* fix: remove dangerously set inner html
2023-03-29 00:20:00 +05:30
Saheb GiriandGitHub c0a471e916 fix: don't allow decimal in the y axis of completed issues graph (#564) 2023-03-29 00:08:34 +05:30
Anmol Singh BhatiaandGitHub e2eeec8f79 feat: completed cycle message added (#565) 2023-03-28 18:48:57 +05:30
kunalv17andGitHub ccd03a4a45 refactor: constants fetchkeys strings to upper case (#563) 2023-03-28 18:28:26 +05:30
pablohashescobar 20bbe74b57 Merge branch 'develop' of github.com:makeplane/plane into develop 2023-03-28 17:47:45 +05:30
kunalv17andGitHub b7b8d3914a style: views list page, chore: views favotire (#562) 2023-03-28 16:48:46 +05:30
afb92ea850 feat: add global search through cmd k (#560)
* feat: cmdk integration

* feat: create view, cycle, project, module and workspace from command k

* feat: user can logout directly from command menu

* feat: user can visit sub page like various settings

* feat: change state of issue from command menu

* chore: add current issue state and minor UX improvements

* refactor: moved change issue state to new file

* feat: change issue priority from command k

* feat: delete issue from command k

* feat: copy issue url to clipboard

* fix: change placeholder when settings page is selected

* chore: remove logout option from cmd k

* feat: add help options to cmd k

* feat: assign issue to member from cmd k

* feat: now assign issue to yourself from cmd k

* chore: implement new cmd k design with icons

* feat: implemented global search feature in the cmd k

* feat: add keyboard acessibility to cmd k list items

* chore: remove console logs

* fix: pages icon in cmd list

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-03-28 14:49:27 +05:30
kunalv17andGitHub fc4d06fe0c style: changed the workspace cards height (#558) 2023-03-28 14:17:24 +05:30
pablohashescobar dd90f26d93 Merge branch 'master' of github.com:makeplane/plane into develop 2023-03-28 13:58:38 +05:30
Aaryan KhandelwalandGitHub fbbf97f3a6 chore: remove docs (#553)
* feat: block sync

* chore: remove docs from repo
2023-03-28 03:54:48 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d9f31a1eb7 chore(deps): bump redis from 4.4.2 to 4.5.3 in /apiserver/requirements (#556)
Bumps [redis](https://github.com/redis/redis-py) from 4.4.2 to 4.5.3.
- [Release notes](https://github.com/redis/redis-py/releases)
- [Changelog](https://github.com/redis/redis-py/blob/master/CHANGES)
- [Commits](https://github.com/redis/redis-py/compare/v4.4.2...v4.5.3)

---
updated-dependencies:
- dependency-name: redis
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-28 03:41:58 +05:30
pablohashescobarandGitHub a06d59f77d feat: sync flag for syncing issues with page blocks (#547) 2023-03-28 01:52:46 +05:30
pablohashescobarandGitHub 722a053461 fix: date for earlier this week (#540) 2023-03-28 01:52:34 +05:30
pablohashescobarandGitHub d6818e74fd feat: page view toggle in project settings (#536) 2023-03-28 01:52:21 +05:30
Aaryan KhandelwalandGitHub 37aade5ef6 fix: order by last created (#550)
* feat: block sync

* fix: order by last created order
2023-03-28 01:52:13 +05:30
Aaryan KhandelwalandGitHub b9e42d116e fix: issue modal overflow (#549)
* feat: block sync

* fix: create issue modal overflow
2023-03-28 01:52:04 +05:30
Aaryan KhandelwalandGitHub e0928d6ec5 fix: default label color (#551)
* feat: block sync

* fix: default label color
2023-03-28 01:50:55 +05:30
Aaryan KhandelwalandGitHub 909ccd578b chore: crisp integration (#552)
* feat: block sync

* chore: crisp integration

* fix: chat with us icon
2023-03-28 01:50:36 +05:30
Aaryan KhandelwalandGitHub 08ee5dc6b1 feat: block sync (#548) 2023-03-28 00:36:20 +05:30
pablohashescobarandGitHub a5a96d9f66 fix: add slack sdk in requirements file (#546) 2023-03-28 00:33:14 +05:30
pablohashescobarandGitHub 691ea0c080 feat: slack bot setup (#545) 2023-03-27 23:37:00 +05:30
pablohashescobarandGitHub 8bd557a743 feat: gzip compressor for performance upgrades (#538) 2023-03-27 23:36:13 +05:30
pablohashescobarandGitHub 35b80b422d fix: gpt task and prompt (#537) 2023-03-27 23:36:00 +05:30
pablohashescobarandGitHub d6ffc3176e fix: global search endpoint to show entities within projects and add filtering through identifiers (#543) 2023-03-27 23:34:04 +05:30
Aaryan KhandelwalandGitHub 3503b22dd9 refactor: pages folder structure (#544)
* refactor: pages folder structure, mutation issues

* fix: block edit and push

* fix: block title placeholder
2023-03-27 23:19:05 +05:30
Aaryan KhandelwalandGitHub e13b679c28 fix: join project screen flicker (#541) 2023-03-27 16:53:31 +05:30
Aaryan KhandelwalandGitHub 21dd2e703b fix: module progress bar (#535) 2023-03-26 11:37:06 +05:30
Aaryan KhandelwalandGitHub 5dd5fe2d09 feat: gpt integration for page block description (#539) 2023-03-26 11:36:10 +05:30
pablohashescobarandGitHub 52d4828e1d fix: issue filters for start date and target date (#530) 2023-03-25 23:57:31 +05:30
pablohashescobarandGitHub c5baa6183c fix: pages endpoint for dates and access (#531)
* fix: pages endpoint for dates and access

* fix: recent pages endpoint

* fix: recent pages endpoint

* fix: date object in recent pages endpoint

* dev: update nomenclature
2023-03-25 23:57:16 +05:30
pablohashescobarandGitHub 69387ffd8c fix: page labels update (#532) 2023-03-25 23:57:06 +05:30
pablohashescobarandGitHub 32ab7951f7 perf: update issue serializer (#534) 2023-03-25 23:56:53 +05:30
Aaryan KhandelwalandGitHub 5d67029b5a feat: pages (#533)
* style: page details

* style: page blocks design

* chore: pages list end points

* feat: add blocks, push blocks to issues

* feat: page labels, color options

* feat: added labels to pages

* fix: update page mutation
2023-03-25 23:39:46 +05:30
pablohashescobarandGitHub 578d724e41 feat: global search endpoint for workspace (#529) 2023-03-25 11:16:33 +05:30
pablohashescobarandGitHub a3a792741f feat: GPT integration (#526)
* feat: GPT integration

* dev: move engine value to env variable
2023-03-25 11:14:26 +05:30
pablohashescobarandGitHub 31624f3ae6 fix: start date and target date issue filter (#525) 2023-03-25 11:14:16 +05:30
pablohashescobarandGitHub 5152deb2d0 fix: model ordering (#521)
* fix: model ordering

* fix: my pages and create by others pages
2023-03-25 11:14:05 +05:30
pablohashescobarandGitHub c6ba93da72 fix: dashboard issues filters (#519)
* fix: upcoming issues filters

* dev: update dashboard endpoint to return correct month
2023-03-25 11:13:14 +05:30
pablohashescobarandGitHub 28b3c999ae dev: update recent pages endpoint and return blocks on page listing (#517)
* dev: update recent pages endpoint and return blocks on page listing

* fix: yesterday's pages
2023-03-25 11:12:54 +05:30
82b9275609 fix: deselecting filter option when clicked again (#528)
* fix: filters dropdown overflowing issue

* filters dropdown z-index

* fix: deselecting filter option when clicked again

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-03-24 23:33:28 +05:30
Dakshesh JainandGitHub f2054b6945 style: removed color text from label modal (#527)
also closing color picker on color select
2023-03-24 23:32:33 +05:30
Dakshesh JainandGitHub f3583f6415 feat: added filter 'created_by' (#524)
* feat: clear filter button

* feat: added filter 'created_by'

* style: text align left for all the issues in filters dropdown
2023-03-24 23:31:56 +05:30
Aaryan KhandelwalandGitHub 02f423bcb6 fix: workspace dashboard (#522)
* chore: completed issues graph

* style: issue stats
2023-03-24 11:06:52 +05:30
Aaryan KhandelwalandGitHub 3d34741356 chore: update view button (#523) 2023-03-24 10:56:06 +05:30
sphynxuxandGitHub 7731ee5de4 Merge pull request #520 from makeplane/feat/custom_error_page
fix: removed unnecessary variable
2023-03-24 01:15:19 +05:30
Aaryan Khandelwal 0ba2c5456a fix: removed unnecessary variable 2023-03-24 01:14:17 +05:30
sphynxuxandGitHub a0ce3f0be6 Merge pull request #518 from makeplane/feat/custom_error_page
feat: custom error page
2023-03-24 01:12:29 +05:30
Aaryan KhandelwalandGitHub 472767ab67 feat: group by created by option (#516) 2023-03-24 01:11:42 +05:30
Aaryan Khandelwal ad60b8774e feat: custom error page 2023-03-24 01:11:33 +05:30
pablohashescobarandGitHub 6c6f9a5bfd fix: dashboard endpoint for overdue and upcoming issues (#502)
* fix: dashboard endpoint for overdue and upcoming issues

* dev: update for upcoming issue to get target date null issues as well

* dev: update the filter
2023-03-24 00:13:48 +05:30
pablohashescobarandGitHub 3056727190 dev: endpoints for my, other, recent and favorite pages, add sort order and color for pages (#499)
* dev: endpoints for my, other, recent and favorite pages, add sort order and color for pages

* dev: fix state attribute error while saving page blocks
2023-03-24 00:13:26 +05:30
pablohashescobarandGitHub 053ae2063e fix: issue filtering keyerror (#494) 2023-03-24 00:13:05 +05:30
pablohashescobarandGitHub 2501c819d3 fix: keyerror on comment create and update (#492) 2023-03-24 00:12:56 +05:30
Aaryan KhandelwalandGitHub 765cfdbf7e fix: kanban loading state (#514) 2023-03-23 23:36:52 +05:30
guru_sainathandGitHub 2f2caaaf6e Feat: Github importer to sync issues, users, and labels with workspace projects. (#509)
* Dev: Github integration with issues and layout integration

* dev: Github Integration route and UI configuration
2023-03-23 23:27:11 +05:30
Aaryan KhandelwalandGitHub 7892a563b7 fix: minor ui fixes (#515)
* fix: sidebar arror positioning

* chore: show empty groups default value as true
2023-03-23 23:25:30 +05:30
Aaryan KhandelwalandGitHub f3b7fc6eb5 fix: filters dropdown overflowing issue (#513)
* fix: filters dropdown overflowing issue

* filters dropdown z-index
2023-03-23 23:25:08 +05:30
Aaryan KhandelwalandGitHub f01f2fb9bd style: my profiles page responsiveness (#512) 2023-03-23 22:55:58 +05:30
Aaryan KhandelwalandGitHub 19434342d3 fix: delete image preview after modal close (#511) 2023-03-23 22:55:12 +05:30
Aaryan KhandelwalandGitHub 6962d7718f chore: updated my issues text (#507) 2023-03-23 22:54:53 +05:30
Anmol Singh BhatiaandGitHub b6a3615f66 feat: completed cycle validation , fix: quick action and kanban fix (#505)
* feat: completed cycle card validation

* fix: unique key to hidden group

* feat: completed cycle sidebar validation

* fix: remove console log from progress chart hover

* feat: kanban and list view completed cycle validation

* feat: quick action validation

* refactor: code refactor

* fix: sidebar draft cycle status
2023-03-23 22:42:08 +05:30
Anmol Singh BhatiaandGitHub 5191fc5f7c style: create workspace bg fix (#510) 2023-03-23 22:41:30 +05:30
Aaryan KhandelwalandGitHub 567afa6d39 fix: kanban drag and drop (#508) 2023-03-23 18:52:18 +05:30
Dakshesh JainandGitHub feb0e40559 refactor: state with group 'completed' or 'cancelled' are collapsed by default (#506) 2023-03-23 18:10:28 +05:30
4a81b988b4 feat: implemented new pages design with bare minimum functionality (#503)
* chore: add page types and page api service

* chore: add create, list, update and delete on pages

* chore: add create, delete and patch page blocks

* feat: add and remove pages to favorite

* fix: made neccessary changes

- used tailwind for hover events
- add error toast alert
- used partial for patch request

* fix: replace absolute positiong with a flex box

* fix: design list page view to match with ui

* feat: add top large textarea for page title and description

* refactor: add page label with types

* feat: add pages grid layout

* feat: add tabs and masonry layout

* fix: build errors

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-03-23 16:12:14 +05:30
Aaryan KhandelwalandGitHub c755907d99 chore: new workspace endpoint (#504)
* chore: new workspace dashboard endpoint

* chore: overdue and upcoming issues
2023-03-23 15:54:59 +05:30
Aaryan KhandelwalandGitHub ad2fa91a2b chore: new global select filters component (#501) 2023-03-23 12:01:50 +05:30
Saheb GiriandGitHub 10e5ba7b3e feat: add pages and page blocks (#495)
* chore: add page types and page api service

* chore: add create, list, update and delete on pages

* chore: add create, delete and patch page blocks

* feat: add and remove pages to favorite

* fix: made neccessary changes

- used tailwind for hover events
- add error toast alert
- used partial for patch request

* fix: replace absolute positiong with a flex box
2023-03-23 11:01:06 +05:30
Aaryan KhandelwalandGitHub d477c19ad9 feat: show empty states toggle button in the views dropdown (#500)
* feat: show empty states toggle button in views dropdown

* refactor: empty state toggle naming convention, feat: hidden groups in section in the kanban board
2023-03-23 02:13:52 +05:30
Aaryan KhandelwalandGitHub 79249c5c9b fix: states order (#498)
* fix: order of states in the kanban board

* fix: state name in list view
2023-03-23 01:00:50 +05:30
pablohashescobarandGitHub 4e3c9397ea feat: page labels and favorites (#487)
* dev: initiate page labels

* dev: page labels

* dev: my pages endpoint
2023-03-22 23:41:30 +05:30
Aaryan KhandelwalandGitHub cd26b2e096 Revert "fix: sidebar and shortcut modal (#493)" (#497)
This reverts commit f615f8ac0c.
2023-03-22 23:40:25 +05:30
pablohashescobarandGitHub 27b1308227 fix: file asset uploading and deleting (#496) 2023-03-22 23:38:47 +05:30
Anmol Singh BhatiaandGitHub f615f8ac0c fix: sidebar and shortcut modal (#493)
* fix: shortcut modal cmd and ctrl fix

* fix: sidebar collapse state ordering
2023-03-22 19:08:04 +05:30
Dakshesh JainandGitHub 031f6443a0 style: kanban loader (#491) 2023-03-22 19:06:36 +05:30
Dakshesh JainandGitHub e48d98cea6 feat: showing progress while page is being loaded (#485) 2023-03-22 19:05:36 +05:30
Dakshesh JainandGitHub a830808f9d refactor: views & filter (#490)
* fix: not saving filters on views detail page

* refactor: using issues endpoint to get issues in views detail page

feat: showing toast alert on saving view
2023-03-22 18:18:19 +05:30
Aaryan KhandelwalandGitHub 9a97c97336 chore: new modules and cycles response (#489) 2023-03-22 18:10:38 +05:30
Aaryan KhandelwalandGitHub 2e346158ba fix: minor ui fixes (#488) 2023-03-22 16:58:32 +05:30
Aaryan KhandelwalandGitHub 283950c8e2 style: views (#486) 2023-03-22 14:47:13 +05:30
vamsi 818d1147d5 dev: new migrations for added fields and introducing Pages 2023-03-22 01:39:53 +05:30
pablohashescobarandGitHub c4594bff01 feat: cycles and modules issues state group percentages (#484)
* dev: state group issue percentage on cycle list

* dev: add issue percentage fields for modules and query updates on cycle apis
2023-03-22 01:36:52 +05:30
pablohashescobarandGitHub 5e81600e38 feat: jira issue importer (#476)
* dev: initialize jira importer

* dev: create service import for jira

* dev: update task to create all users for project and workspace and also create assignees when importing bulk assignees

* dev: create bulk modules import endpoint for jira epics

* dev: create bulk module issues when importing modules
2023-03-22 01:36:38 +05:30
pablohashescobarandGitHub 846e73e3b8 feat: add issue filters for cycles and modules (#475) 2023-03-22 01:36:06 +05:30
pablohashescobarandGitHub 4dc76eac19 fix: project url max length (#471) 2023-03-22 01:35:53 +05:30
pablohashescobarandGitHub f7dbc5e9c0 feat: page and page-blocks (#468)
* dev: initiate paper models

* feat: page and page-blocks

* dev: page id filter for page blocks
2023-03-22 01:34:10 +05:30
pablohashescobarandGitHub 025c8b3835 feat: sub issue toggle (#464) 2023-03-22 01:33:56 +05:30
pablohashescobarandGitHub 9339138c0e feat: user workspace dashboard endpoint (#461)
* dev: initiate new dashboard endpoint

* fix: updated issue week for this count and pending issues

* dev: add try catch block

* dev: add exception

* dev: issue state distribution endpoint

* dev: move from state name to state group

* dev: add overdue issues and upcoming issues
2023-03-22 01:33:44 +05:30
pablohashescobarandGitHub 988b27f909 refactor: users endpoint to return invites and assigned issues count (#457) 2023-03-22 01:33:31 +05:30
pablohashescobarandGitHub a3bacbfef1 fix: workspace invitation datetime import (#456)
* chore: update the endpoint to return last 6 months data

* dev: add datetime import
2023-03-22 01:33:19 +05:30
pablohashescobarandGitHub 698718aa8d chore: rename analytics env variable keys (#455) 2023-03-22 01:33:06 +05:30
pablohashescobarandGitHub 7fd775ab05 chore: update the endpoint to return last 6 months data (#453) 2023-03-22 01:32:51 +05:30
pablohashescobarandGitHub e5f8c94de8 feat: add flag for toggling issue views (#452) 2023-03-22 01:32:37 +05:30
Dakshesh JainandGitHub 9c388d8e50 refactor: setting filters as null if value is an empty array (#483)
* refractor: added params to fetch key

* feat: create views directly from views list page

fix: selected filter not showing up in multi-level dropdown, refactor: arranged imports

* refactor: setting filters as null if value is an empty array
2023-03-21 17:11:04 +05:30
Dakshesh JainandGitHub bf09673d09 refactor: making unsplash api request from api folder (#482)
* refractor: added params to fetch key

* feat: create views directly from views list page

fix: selected filter not showing up in multi-level dropdown, refactor: arranged imports

* refactor: making unsplash api request from api folder

to hide acces key from client side
2023-03-21 16:31:01 +05:30
Dakshesh JainandGitHub 5869c91d70 fix: added redirection to sign-in page if fetching user was a fail in the context (#481)
* refractor: added params to fetch key

* feat: create views directly from views list page

fix: selected filter not showing up in multi-level dropdown, refactor: arranged imports

* fix: added redirection to sign-in page if fetching user was a fail in the context
2023-03-21 12:48:17 +05:30
Dakshesh JainandGitHub 53df658b60 refactor: added params to filters, and removed manual mutation (#480)
* refractor: added params to fetch key

* feat: create views directly from views list page

fix: selected filter not showing up in multi-level dropdown, refactor: arranged imports

* refactor: added params to filters, and removed manual mutation
2023-03-21 12:47:47 +05:30
Anmol Singh BhatiaandGitHub 505b14e3a6 fix: ui fixes and improvement (#479)
* fix: list view longer title tooltip fix

* fix: module and cycle sidebar ui improvement
2023-03-21 12:47:10 +05:30
Anmol Singh BhatiaandGitHub b96d40f106 style: auth screens (#478)
* style: sign in page

* style: github and google sign

* style: sign with code and password

* style: not a member and not authorized for project setting

* style: join project icon

* chore: comment removed
2023-03-21 12:46:12 +05:30
Dakshesh JainandGitHub 68150a9d2b feat: create views directly from the views list page (#472)
* refractor: added params to fetch key

* feat: create views directly from views list page

fix: selected filter not showing up in multi-level dropdown, refactor: arranged imports
2023-03-18 11:34:49 +05:30
Dakshesh JainandGitHub e6b0012fe2 fix: mutation on issue create (#473)
* refractor: added params to fetch key

* feat: create views directly from views list page

fix: selected filter not showing up in multi-level dropdown, refactor: arranged imports

* fix: mutation on project create
2023-03-18 11:34:29 +05:30
Anmol Singh BhatiaandGitHub 5739d95ab4 style: onboarding, chore: refactoring (#474)
* style: onboarding screens

* style: onboarding card component and refactoring

* fix: onboarding card text fix

* fix: merge conflict fix
2023-03-18 11:34:09 +05:30
pablohashescobarandGitHub 350e183375 dev: update importer task to create user automatically and adding it to project and workspace (#467) 2023-03-17 22:57:10 +05:30
Aaryan KhandelwalandGitHub e7ef6275cd style: new buttons added (#470) 2023-03-17 10:40:38 +05:30
Aaryan KhandelwalandGitHub 4de0abfc22 fix: minor ui fixes (#469) 2023-03-17 10:39:06 +05:30
Aaryan KhandelwalandGitHub 5f796e732a fix: bot activity and comment log name (#463)
* fix: github bot activity log details

* fix: bot comment details

* fix: updated bot logs

* refactor: bot name logic
2023-03-17 10:38:01 +05:30
Dakshesh JainandGitHub 0fb9a14f15 refractor: added params to fetch key (#465) 2023-03-16 18:15:08 +05:30
Aaryan KhandelwalandGitHub 23c468786d style: filter issues dropdown (#466) 2023-03-16 18:14:07 +05:30
Dakshesh JainandGitHub 0f06589b83 style: made new issue filter dropdown (#462) 2023-03-16 16:27:18 +05:30
Aaryan KhandelwalandGitHub a84abc60b2 style: workspace dashboard (#460)
* style: workspace dashboard

* feat: activity graph

* chore: change tile colors for activity graph

* fix: activity graph tiles order, color

* style: activity intensity
2023-03-16 15:53:49 +05:30
Aaryan KhandelwalandGitHub 27324ddd93 fix: github description not appearing (#459) 2023-03-16 15:53:25 +05:30
Aaryan KhandelwalandGitHub d413dd1169 chore: update user profile stats (#458) 2023-03-16 15:53:09 +05:30
Dakshesh JainandGitHub ef0e326ca0 feat: issues filter using views (#448)
* fix: made basic UI for views, binded services and logic for views

* feat: views list, delete view, and conditionally updating filters or my view props
2023-03-16 14:07:19 +05:30
Aaryan KhandelwalandGitHub 96ad751e11 style: new workspace dashboard design (#454)
* style: workspace dashboard

* feat: activity graph
2023-03-16 01:36:21 +05:30
vamsi 8370511a66 dev: new migrations Importer, IssueView, IssueViewFavorite and a few alterations 2023-03-16 00:41:50 +05:30
pablohashescobarandGitHub 836dc4027b feat: jitsu events for sign in and sign up (#423)
* feat: jitsu events for sign in and sign up

* dev: update event data
2023-03-15 23:25:38 +05:30
pablohashescobarandGitHub 88754e6fc0 dev: separate endpoints for workspace assets and user assets (#420) 2023-03-15 23:25:23 +05:30
pablohashescobarandGitHub b6ee197b40 feat: issue filter views (#418)
* dev: views initiated

* dev: refactor filtering logic

* dev: move state grouping filter to util function

* dev: view issues create endpoint and update on filters for time

* dev: rename views to issue views

* dev: rename in serilaizer and views

* dev: update issue filters

* dev: update filter

* feat: create issue favorites

* dev: update query keys

* dev: update create and update method
2023-03-15 23:25:09 +05:30
pablohashescobarandGitHub 46f6b61928 refactor: grouper function to fix priority keys (#415) 2023-03-15 23:24:55 +05:30
pablohashescobarandGitHub 5d8f2b6b75 feat: github importer (#425)
* dev: init github importer

* dev: add endpoint for creating import

* dev: create endpoint to bulk create issues

* dev: bulk issue importer

* dev: bulk create endpoints for labels and updates in issue bulk create endpoint to create labels and links

* dev: add comments in bluk create

* dev: status import endpoint and user invitaion workflow

* dev: initiate github repo sync

* dev: bulk issue sync endpoint and fix key issue in bg task

* dev: update endpoints for service imports

* dev: update labels logic

* dev: update importer task

* dev: bulk issue activities

* dev: update importer task for mapped users

* dev: update importer endpoint to send github token

* dev: update bulk import endpoint

* fix: workspace get query

* dev: update bulk import endpoints
2023-03-15 23:24:44 +05:30
pablohashescobarandGitHub d3ca8560fc fix: project list ordering (#427) 2023-03-15 23:24:26 +05:30
pablohashescobarandGitHub ab9e0cf559 refactor: update response structure for cycle issue and module issues (#432) 2023-03-15 23:21:37 +05:30
pablohashescobarandGitHub c07cfee018 refactor: update favorites for project, cycle and module (#434)
* dev: refactor favorites

* dev: fix typo
2023-03-15 23:21:23 +05:30
pablohashescobarandGitHub ed8f0b8473 feat: user activity graph (#437)
* feat: user activity graph

* dev: issue completed and activity graph
2023-03-15 23:21:08 +05:30
pablohashescobarandGitHub 0082a98d53 dev: add cycle date validation (#442) 2023-03-15 23:20:49 +05:30
pablohashescobarandGitHub d6aadb115d refactor: users endpoint to return invites and assigned issues count (#449) 2023-03-15 23:20:31 +05:30
Aaryan KhandelwalandGitHub 4639ab3d9c fix: workspace dropdown (#447)
* fix: workspace dropdown links

* fix: module progress round off
2023-03-15 18:50:37 +05:30
Aaryan KhandelwalandGitHub c11bf7c7de feat: progress bar for module (#446) 2023-03-15 18:00:40 +05:30
Anmol Singh BhatiaandGitHub ae8902e815 style: sidebar icon color (#444) 2023-03-15 17:41:52 +05:30
Anmol Singh BhatiaandGitHub 164072e3cc fix: date checker edge case fix (#443) 2023-03-15 17:41:24 +05:30
928ebdf632 fix: mutation for issue update on both kanban & list (#436)
* refactor: issues filter logic

* fix: removed fetch logic from hooks

* feat: filter by assignee and label

* chore: remove filter buttons

* feat: filter options

* fix: mutation for issue update on both kanban & list

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-03-15 11:44:44 +05:30
Anmol Singh BhatiaandGitHub 636e8e6c60 style: empty state global component (#435) 2023-03-15 11:01:54 +05:30
Aaryan KhandelwalandGitHub bfab4865cd chore: update user file assets endpoint (#438)
* chore: new service for user assets

* chore: update user file assets endpoint
2023-03-15 11:00:42 +05:30
Aaryan KhandelwalandGitHub dbd6de0988 feat: views template created (#439) 2023-03-15 11:00:05 +05:30
Aaryan KhandelwalandGitHub 32d37ec45e refactor: custom search select component (#440) 2023-03-15 10:59:43 +05:30
Aaryan KhandelwalandGitHub bcd2ac1317 feat: issue filters dropdown created (#441) 2023-03-15 10:59:23 +05:30
Dakshesh JainandGitHub 0117ccfca2 fix: made project list authenticated (#428) 2023-03-14 12:21:06 +05:30
Aaryan KhandelwalandGitHub 0ba81a10f1 style: made the paddings and text sizes smaller (#433) 2023-03-14 12:18:14 +05:30
Aaryan KhandelwalandGitHub 3f5bbf336c fix: truncate text function (#431) 2023-03-14 12:16:38 +05:30
Aaryan KhandelwalandGitHub d6d51c2f43 fix: modules sidebar lead select not working (#429) 2023-03-14 12:16:26 +05:30
Anmol Singh BhatiaandGitHub d5d64e09d4 style: design (#430)
* style: shortcut modal

* style: feature setting module icon

* style: delete issue modal

* style: delete project modal

* style: sidebar prompt for chart and other info

* fix: create issue modal state icon

* fix: workspace dropdown
2023-03-13 23:38:43 +05:30
Narayana VadapalliandGitHub 7d7683ae6f Merge pull request #424 from pinarruiz/feature/upload-images
Github actions to push images
2023-03-13 10:20:37 +05:30
Narayana VadapalliandGitHub fcd64de8af Update push-image-frontend.yml 2023-03-13 10:19:17 +05:30
Narayana VadapalliandGitHub b2765d47b4 Update push-image-backend.yml 2023-03-13 10:15:05 +05:30
Alejandro Pinar Ruiz aca0c251b8 Github actions to push images 2023-03-12 20:35:47 +01:00
pablohashescobarandGitHub 6de6522a41 chore: permissions for api endpoints (#419) 2023-03-11 23:51:06 +05:30
Vihar KuramaandGitHub bff89ee4c6 Merge pull request #422 from makeplane/feat/issue_templates
feat: new issue templates
2023-03-11 23:50:16 +05:30
pablohashescobar 7744d9b69a feat: new issue templates 2023-03-11 23:49:02 +05:30
Aaryan KhandelwalandGitHub 441cf39d2c refactor: global workspace form (#421) 2023-03-11 17:23:23 +05:30
Anmol Singh BhatiaandGitHub 4a7f80712b style: workspace sidebar (#417) 2023-03-10 16:05:10 +05:30
Aaryan KhandelwalandGitHub c7923f6d44 feat: added load more button to github repos dropdown (#414) 2023-03-10 16:03:49 +05:30
Anmol Singh BhatiaandGitHub 4fad685ec8 style: workspace dropdown (#416)
* style: workspace dropdown

* style: workspace dropdown hover fix
2023-03-10 12:32:29 +05:30
Anmol Singh BhatiaandGitHub 704b7d02ef style : ui fixes (#412)
* fix: kanban view vertical scroll fix

* fix: delete option remove from my issue page

* fix: my issue filter key renamed with id

* fix: sidebar ellipsis alignment

* fix: cycle card favorite icon alignment

* style: icon added in card options

* fix: progress icon alignment

* style: my issue page list view
2023-03-09 22:50:34 +05:30
Anmol Singh BhatiaandGitHub 4e9149a27c style: empty cycle state (#410) 2023-03-09 22:49:03 +05:30
pablohashescobarandGitHub 0416e07f46 refactor: self hosting setup (#411)
* merge-commit: self hosted updates

* dev: updates in self hosting setup

* dev: update script to get the instance IP

* dev: update script to generate backend secret key
2023-03-09 20:49:12 +05:30
Anmol Singh BhatiaandGitHub e3e57df4a2 style: list view (#409)
* style: list view

* style: list board header spacing fix
2023-03-09 16:05:25 +05:30
752 changed files with 49324 additions and 19416 deletions
+20
View File
@@ -0,0 +1,20 @@
# Replace with your instance Public IP
NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS=
NEXT_PUBLIC_GOOGLE_CLIENTID=""
NEXT_PUBLIC_GITHUB_APP_NAME=""
NEXT_PUBLIC_GITHUB_ID=""
NEXT_PUBLIC_SENTRY_DSN=""
NEXT_PUBLIC_ENABLE_OAUTH=0
NEXT_PUBLIC_ENABLE_SENTRY=0
NEXT_PUBLIC_ENABLE_SESSION_RECORDER=0
NEXT_PUBLIC_TRACK_EVENTS=0
NEXT_PUBLIC_SLACK_CLIENT_ID=""
EMAIL_HOST=""
EMAIL_HOST_USER=""
EMAIL_HOST_PASSWORD=""
AWS_REGION=""
AWS_ACCESS_KEY_ID=""
AWS_SECRET_ACCESS_KEY=""
AWS_S3_BUCKET_NAME=""
OPENAI_API_KEY=""
GPT_ENGINE=""
+65
View File
@@ -0,0 +1,65 @@
name: Bug report
description: Create a bug report to help us improve Plane
title: "[bug]: "
labels: [bug, need testing]
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to fill out this bug report.
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered
options:
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
label: Current behavior
description: A concise description of what you're experiencing and what you expect
placeholder: |
When I do <X>, <Y> happens and I see the error message attached below:
```...```
What I expect is <Z>
validations:
required: true
- type: textarea
attributes:
label: Steps to reproduce
description: Add steps to reproduce this behaviour, include console or network logs and screenshots
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: dropdown
id: env
attributes:
label: Environment
options:
- Production
- Deploy preview
validations:
required: true
type: dropdown
id: browser
attributes:
label: Browser
options:
- Google Chrome
- Mozilla Firefox
- Safari
- Other
- type: dropdown
id: version
attributes:
label: Version
options:
- Cloud
- Self-hosted
- Local
validations:
required: true
@@ -0,0 +1,28 @@
name: Feature request
description: Suggest a feature to improve Plane
title: "[feature]: "
labels: [feature]
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to request a feature for Plane
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue related to this feature request already exists
options:
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
label: Summary
description: One paragraph description of the feature
validations:
required: true
- type: textarea
attributes:
label: Why should this be worked on?
description: A concise description of the problems or use cases for this feature request
validations:
required: true
+6
View File
@@ -0,0 +1,6 @@
contact_links:
- name: Help and support
about: Reach out to us on our Discord server or GitHub discussions.
- name: Dedicated support
url: mailto:support@plane.so
about: Write to us if you'd like dedicated support using Plane
+77
View File
@@ -0,0 +1,77 @@
name: Build and Push Backend Docker Image
on:
push:
branches:
- 'develop'
- 'master'
tags:
- '*'
jobs:
build_push_backend:
name: Build and Push Api Server Docker Image
runs-on: ubuntu-20.04
steps:
- name: Check out the repo
uses: actions/checkout@v3.3.0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
with:
platforms: linux/arm64,linux/amd64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.5.0
- name: Login to GitHub Container Registry
uses: docker/login-action@v2.1.0
with:
registry: "ghcr.io"
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v2.1.0
with:
registry: "registry.hub.docker.com"
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker (Docker Hub)
id: ghmeta
uses: docker/metadata-action@v4.3.0
with:
images: makeplane/plane-backend
- name: Extract metadata (tags, labels) for Docker (Github)
id: dkrmeta
uses: docker/metadata-action@v4.3.0
with:
images: ghcr.io/${{ github.repository }}-backend
- name: Build and Push to GitHub Container Registry
uses: docker/build-push-action@v4.0.0
with:
context: ./apiserver
file: ./apiserver/Dockerfile.api
platforms: linux/arm64,linux/amd64
push: true
cache-from: type=gha
cache-to: type=gha
tags: ${{ steps.ghmeta.outputs.tags }}
labels: ${{ steps.ghmeta.outputs.labels }}
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v4.0.0
with:
context: ./apiserver
file: ./apiserver/Dockerfile.api
platforms: linux/arm64,linux/amd64
push: true
cache-from: type=gha
cache-to: type=gha
tags: ${{ steps.dkrmeta.outputs.tags }}
labels: ${{ steps.dkrmeta.outputs.labels }}
+77
View File
@@ -0,0 +1,77 @@
name: Build and Push Frontend Docker Image
on:
push:
branches:
- 'develop'
- 'master'
tags:
- '*'
jobs:
build_push_frontend:
name: Build Frontend Docker Image
runs-on: ubuntu-20.04
steps:
- name: Check out the repo
uses: actions/checkout@v3.3.0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
with:
platforms: linux/arm64,linux/amd64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.5.0
- name: Login to Github Container Registry
uses: docker/login-action@v2.1.0
with:
registry: "ghcr.io"
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v2.1.0
with:
registry: "registry.hub.docker.com"
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker (Docker Hub)
id: ghmeta
uses: docker/metadata-action@v4.3.0
with:
images: makeplane/plane-frontend
- name: Extract metadata (tags, labels) for Docker (Github)
id: meta
uses: docker/metadata-action@v4.3.0
with:
images: ghcr.io/${{ github.repository }}-frontend
- name: Build and Push to GitHub Container Registry
uses: docker/build-push-action@v4.0.0
with:
context: .
file: ./apps/app/Dockerfile.web
platforms: linux/arm64,linux/amd64
push: true
cache-from: type=gha
cache-to: type=gha
tags: ${{ steps.ghmeta.outputs.tags }}
labels: ${{ steps.ghmeta.outputs.labels }}
- name: Build and Push to Docker Container Registry
uses: docker/build-push-action@v4.0.0
with:
context: .
file: ./apps/app/Dockerfile.web
platforms: linux/arm64,linux/amd64
push: true
cache-from: type=gha
cache-to: type=gha
tags: ${{ steps.dkrmeta.outputs.tags }}
labels: ${{ steps.dkrmeta.outputs.labels }}
+19 -1
View File
@@ -3,6 +3,7 @@ RUN apk add --no-cache libc6-compat
RUN apk update
# Set working directory
WORKDIR /app
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
RUN yarn global add turbo
COPY . .
@@ -16,7 +17,7 @@ FROM node:18-alpine AS installer
RUN apk add --no-cache libc6-compat
RUN apk update
WORKDIR /app
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
# First install the dependencies (as they change less often)
COPY .gitignore .gitignore
COPY --from=builder /app/out/json/ .
@@ -26,9 +27,16 @@ RUN yarn install
# Build the project
COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
COPY replace-env-vars.sh /usr/local/bin/
USER root
RUN chmod +x /usr/local/bin/replace-env-vars.sh
RUN yarn turbo run build --filter=app
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL}
FROM python:3.11.1-alpine3.17 AS backend
@@ -108,6 +116,16 @@ COPY nginx/nginx-single-docker-image.conf /etc/nginx/http.d/default.conf
COPY nginx/supervisor.conf /code/supervisor.conf
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
USER root
COPY replace-env-vars.sh /usr/local/bin/
COPY start.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/replace-env-vars.sh
RUN chmod +x /usr/local/bin/start.sh
CMD ["supervisord","-c","/code/supervisor.conf"]
+113 -20
View File
@@ -2,50 +2,143 @@
<p align="center">
<a href="https://plane.so">
<img src="https://res.cloudinary.com/dgxawjvpo/image/upload/v1673379660/Plane/plane-logo_0m83xue7R_f0v9r9.png" alt="Plane Logo" width="350">
<img src="https://res.cloudinary.com/toolspacedev/image/upload/v1680596414/Plane/Plane_Icon_Blue_on_White_150x150_muysa3.jpg" alt="Plane Logo" width="70">
</a>
</p>
<h3 align="center"><b>Plane</b></h3>
<p align="center"><b>Open-source, self-hosted project planning tool</b></p>
<p align="center">
<a href="https://discord.com/invite/A92xrEGCge">
<img alt="Discord" src="https://img.shields.io/discord/1031547764020084846?color=5865F2&label=Discord&style=for-the-badge" />
</a>
<img alt="Discord" src="https://img.shields.io/github/commit-activity/m/makeplane/plane?style=for-the-badge" />
</p>
<br />
Plane is an open-source project planning tool that is designed to help individuals and teams streamline their issues, sprints, and product roadmaps. It is easy to use and can be accessed by anyone, making it an ideal choice for a wide range of projects and organizations.
<br /> <br />
<p>
<a href="https://app.plane.so/" target="_blank">
<img
src="https://res.cloudinary.com/toolspacedev/image/upload/v1680599798/Plane/plane_1_1_tnb32j.png"
alt="Plane Screens"
width="100%"
/>
</a>
</p>
> Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/29tPNhaV) or GitHub issues, and we will use your feedback to improve on our upcoming releases.
Meet [Plane](https://plane.so). An open-source software development tool to manage issues, sprints, and product roadmaps with peace of mind 🧘‍♀️.
## Getting Started
Visit https://app.plane.so to get started with Plane.
> Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve on our upcoming releases.
## Documentation
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting).
## ⚡️ Quick start with Docker Compose
### Docker Compose Setup
- Clone the Repository
```bash
git clone https://github.com/makeplane/plane
```
- Change Directory
```bash
cd plane
```
- Run setup.sh
```bash
./setup.sh localhost
```
> If running in a cloud env replace localhost with public facing IP address of the VM
- Export Environment Variables
```bash
set -a
source .env
set +a
```
- Run Docker compose up
```bash
docker-compose -f docker-compose-hub.yml up
```
<strong>You can use the default email and password for your first login `captain@plane.so` and `password123`.</strong>
## 🚀 Features
* **Issue Planning and Tracking**: Quickly create issues and add details using a powerful rich text editor that supports file uploads. Add sub-properties and references to issues for better organization and tracking.
* **Issue Attachments**: Collaborate effectively by attaching files to issues, making it easy for your team to find and share important project-related documents.
* **Layouts**: Customize your project view with your preferred layout - choose from List, Kanban, or Calendar to visualize your project in a way that makes sense to you.
* **Cycles**: Plan sprints with Cycles to keep your team on track and productive. Gain insights into your project's progress with burn-down charts and other useful features.
* **Modules**: Break down your large projects into smaller, more manageable modules. Assign modules between teams to easily track and plan your project's progress.
* **Views**: Create custom filters to display only the issues that matter to you. Save and share your filters in just a few clicks.
* **Pages**: Plane pages function as an AI-powered notepad, allowing you to easily document issues, cycle plans, and module details, and then synchronize them with your issues.
* **Command K**: Enjoy a better user experience with the new Command + K menu. Easily manage and navigate through your projects from one convenient location.
* **GitHub Sync**: Streamline your planning process by syncing your GitHub issues with Plane. Keep all your issues in one place for better tracking and collaboration.
## 📸 Screenshots
<p>
<a href="https://app.plane.so/" target="_blank">
<img
src="https://res.cloudinary.com/toolspacedev/image/upload/v1680601719/Plane/plane_2_iqao52.png"
alt="Plane Issue Details"
width="100%"
/>
</a>
</p>
<p>
<a href="https://app.plane.so/" target="_blank">
<img
src="https://res.cloudinary.com/toolspacedev/image/upload/v1680604273/Plane/plane_5_1_nwsl3a.png"
alt="Plane Cycles and Modules"
width="100%"
/>
</a>
</p>
<p>
<a href="https://app.plane.so/" target="_blank">
<img
src="https://res.cloudinary.com/toolspacedev/image/upload/v1680601713/Plane/plane_4_cqm0g8.png"
alt="Plane Quick Lists"
width="100%"
/>
</a>
</p>
<p>
<a href="https://app.plane.so/" target="_blank">
<img
src="https://res.cloudinary.com/toolspacedev/image/upload/v1680601712/Plane/plane_3_1_cu4fsc.png"
alt="Plane Command K"
width="100%"
/>
</a>
</p>
## 📚Documentation
For full documentation, visit [docs.plane.so](https://docs.plane.so/)
To see how to Contribute, visit [here](https://github.com/makeplane/plane/blob/master/CONTRIBUTING.md).
## Status
- [x] Early Community Previews: We are open-sourcing and sharing the development version of Plane
- [ ] Alpha: We are testing Plane with a closed set of customers
- [ ] Public Alpha: Anyone can sign up over at [app.plane.so](https://app.plane.so). But go easy on us, there are a few hiccups
- [ ] Public Beta: Stable enough for most non-enterprise use-cases
- [ ] Public: Production-ready
## Community
## ❤️ Community
The Plane community can be found on GitHub Discussions, where you can ask questions, voice ideas, and share your projects.
To chat with other community members you can join the [Plane Discord](https://discord.com/invite/q9HKAdau).
To chat with other community members you can join the [Plane Discord](https://discord.com/invite/A92xrEGCge).
Our [Code of Conduct](https://github.com/makeplane/plane/blob/master/CODE_OF_CONDUCT.md) applies to all Plane community channels.
## Security
## ⛓️ Security
If you believe you have found a security vulnerability in Plane, we encourage you to responsibly disclose this and not open a public issue. We will investigate all legitimate reports. Email security@plane.so to disclose any security vulnerabilities.
-22
View File
@@ -1,22 +0,0 @@
SECRET_KEY="<-- django secret -->"
DJANGO_SETTINGS_MODULE="plane.settings.production"
# Database
DATABASE_URL=postgres://plane:plane@db:5432/plane
# Cache
REDIS_URL=redis://redis:6379/
# SMPT
EMAIL_HOST="<-- email smtp -->"
EMAIL_HOST_USER="<-- email host user -->"
EMAIL_HOST_PASSWORD="<-- email host password -->"
# AWS
AWS_REGION="<-- aws region -->"
AWS_ACCESS_KEY_ID="<-- aws access key -->"
AWS_SECRET_ACCESS_KEY="<-- aws secret acess key -->"
AWS_S3_BUCKET_NAME="<-- aws s3 bucket name -->"
# FE
WEB_URL="localhost/"
# OAUTH
GITHUB_CLIENT_SECRET="<-- github secret -->"
# Flags
DISABLE_COLLECTSTATIC=1
DOCKERIZED=1
+1 -1
View File
@@ -1,2 +1,2 @@
web: gunicorn -w 4 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:$PORT --config gunicorn.config.py --max-requests 10000 --max-requests-jitter 1000 --access-logfile -
worker: python manage.py rqworker
worker: celery -A plane worker -l info
+89 -1
View File
@@ -3,7 +3,15 @@ import uuid
import random
from django.contrib.auth.hashers import make_password
from plane.db.models import ProjectIdentifier
from plane.db.models import Issue, IssueComment, User, Project
from plane.db.models import (
Issue,
IssueComment,
User,
Project,
ProjectMember,
Label,
Integration,
)
# Update description and description html values for old descriptions
@@ -134,3 +142,83 @@ def update_project_cover_images():
except Exception as e:
print(e)
print("Failed")
def update_user_view_property():
try:
project_members = ProjectMember.objects.all()
updated_project_members = []
for project_member in project_members:
project_member.default_props = {
"filters": {"type": None},
"orderBy": "-created_at",
"collapsed": True,
"issueView": "list",
"filterIssue": None,
"groupByProperty": None,
"showEmptyGroups": True,
}
updated_project_members.append(project_member)
ProjectMember.objects.bulk_update(
updated_project_members, ["default_props"], batch_size=100
)
print("Success")
except Exception as e:
print(e)
print("Failed")
def update_label_color():
try:
labels = Label.objects.filter(color="")
updated_labels = []
for label in labels:
label.color = "#" + "%06x" % random.randint(0, 0xFFFFFF)
updated_labels.append(label)
Label.objects.bulk_update(updated_labels, ["color"], batch_size=100)
print("Success")
except Exception as e:
print(e)
print("Failed")
def create_slack_integration():
try:
_ = Integration.objects.create(provider="slack", network=2, title="Slack")
print("Success")
except Exception as e:
print(e)
print("Failed")
def update_integration_verified():
try:
integrations = Integration.objects.all()
updated_integrations = []
for integration in integrations:
integration.verified = True
updated_integrations.append(integration)
Integration.objects.bulk_update(
updated_integrations, ["verified"], batch_size=10
)
print("Success")
except Exception as e:
print(e)
print("Failed")
def update_start_date():
try:
issues = Issue.objects.filter(state__group__in=["started", "completed"])
updated_issues = []
for issue in issues:
issue.start_date = issue.created_at.date()
updated_issues.append(issue)
Issue.objects.bulk_update(updated_issues, ["start_date"], batch_size=500)
print("Success")
except Exception as e:
print(e)
print("Failed")
+1 -2
View File
@@ -2,5 +2,4 @@
set -e
python manage.py wait_for_db
python manage.py migrate
python manage.py rqworker
celery -A plane worker -l info
+3
View File
@@ -0,0 +1,3 @@
from .celery import app as celery_app
__all__ = ('celery_app',)
+16 -2
View File
@@ -10,6 +10,8 @@ from .workspace import (
WorkSpaceMemberSerializer,
TeamSerializer,
WorkSpaceMemberInviteSerializer,
WorkspaceLiteSerializer,
WorkspaceThemeSerializer,
)
from .project import (
ProjectSerializer,
@@ -18,10 +20,11 @@ from .project import (
ProjectMemberInviteSerializer,
ProjectIdentifierSerializer,
ProjectFavoriteSerializer,
ProjectLiteSerializer,
)
from .state import StateSerializer
from .state import StateSerializer, StateLiteSerializer
from .shortcut import ShortCutSerializer
from .view import ViewSerializer
from .view import IssueViewSerializer, IssueViewFavoriteSerializer
from .cycle import CycleSerializer, CycleIssueSerializer, CycleFavoriteSerializer
from .asset import FileAssetSerializer
from .issue import (
@@ -38,6 +41,8 @@ from .issue import (
IssueFlatSerializer,
IssueStateSerializer,
IssueLinkSerializer,
IssueLiteSerializer,
IssueAttachmentSerializer,
)
from .module import (
@@ -57,4 +62,13 @@ from .integration import (
GithubRepositorySerializer,
GithubRepositorySyncSerializer,
GithubCommentSyncSerializer,
SlackProjectSyncSerializer,
)
from .importer import ImporterSerializer
from .page import PageSerializer, PageBlockSerializer, PageFavoriteSerializer
from .estimate import EstimateSerializer, EstimatePointSerializer, EstimateReadSerializer
from .analytic import AnalyticViewSerializer
@@ -0,0 +1,30 @@
from .base import BaseSerializer
from plane.db.models import AnalyticView
from plane.utils.issue_filters import issue_filters
class AnalyticViewSerializer(BaseSerializer):
class Meta:
model = AnalyticView
fields = "__all__"
read_only_fields = [
"workspace",
"query",
]
def create(self, validated_data):
query_params = validated_data.get("query_dict", {})
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
validated_data["query"] = dict()
return AnalyticView.objects.create(**validated_data)
def update(self, instance, validated_data):
query_params = validated_data.get("query_data", {})
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
validated_data["query"] = dict()
validated_data["query"] = issue_filters(query_params, "PATCH")
return super().update(instance, validated_data)
+33
View File
@@ -5,12 +5,45 @@ from rest_framework import serializers
from .base import BaseSerializer
from .user import UserLiteSerializer
from .issue import IssueStateSerializer
from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer
from plane.db.models import Cycle, CycleIssue, CycleFavorite
class CycleSerializer(BaseSerializer):
owned_by = UserLiteSerializer(read_only=True)
is_favorite = serializers.BooleanField(read_only=True)
total_issues = serializers.IntegerField(read_only=True)
cancelled_issues = serializers.IntegerField(read_only=True)
completed_issues = serializers.IntegerField(read_only=True)
started_issues = serializers.IntegerField(read_only=True)
unstarted_issues = serializers.IntegerField(read_only=True)
backlog_issues = serializers.IntegerField(read_only=True)
assignees = serializers.SerializerMethodField(read_only=True)
total_estimates = serializers.IntegerField(read_only=True)
completed_estimates = serializers.IntegerField(read_only=True)
started_estimates = serializers.IntegerField(read_only=True)
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
def get_assignees(self, obj):
members = [
{
"avatar": assignee.avatar,
"first_name": assignee.first_name,
"id": assignee.id,
}
for issue_cycle in obj.issue_cycle.all()
for assignee in issue_cycle.issue.assignees.all()
]
# Use a set comprehension to return only the unique objects
unique_objects = {frozenset(item.items()) for item in members}
# Convert the set back to a list of dictionaries
unique_list = [dict(item) for item in unique_objects]
return unique_list
class Meta:
model = Cycle
@@ -0,0 +1,44 @@
# Module imports
from .base import BaseSerializer
from plane.db.models import Estimate, EstimatePoint
from plane.api.serializers import WorkspaceLiteSerializer, ProjectLiteSerializer
class EstimateSerializer(BaseSerializer):
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
class Meta:
model = Estimate
fields = "__all__"
read_only_fields = [
"workspace",
"project",
]
class EstimatePointSerializer(BaseSerializer):
class Meta:
model = EstimatePoint
fields = "__all__"
read_only_fields = [
"estimate",
"workspace",
"project",
]
class EstimateReadSerializer(BaseSerializer):
points = EstimatePointSerializer(read_only=True, many=True)
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
class Meta:
model = Estimate
fields = "__all__"
read_only_fields = [
"points",
"name",
"description",
]
@@ -0,0 +1,16 @@
# Module imports
from .base import BaseSerializer
from .user import UserLiteSerializer
from .project import ProjectLiteSerializer
from .workspace import WorkspaceLiteSerializer
from plane.db.models import Importer
class ImporterSerializer(BaseSerializer):
initiated_by_detail = UserLiteSerializer(source="initiated_by", read_only=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
class Meta:
model = Importer
fields = "__all__"
@@ -5,3 +5,4 @@ from .github import (
GithubIssueSyncSerializer,
GithubCommentSyncSerializer,
)
from .slack import SlackProjectSyncSerializer
@@ -0,0 +1,14 @@
# Module imports
from plane.api.serializers import BaseSerializer
from plane.db.models import SlackProjectSync
class SlackProjectSyncSerializer(BaseSerializer):
class Meta:
model = SlackProjectSync
fields = "__all__"
read_only_fields = [
"project",
"workspace",
"workspace_integration",
]
+83 -10
View File
@@ -4,10 +4,10 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .user import UserLiteSerializer
from .state import StateSerializer
from .state import StateSerializer, StateLiteSerializer
from .user import UserLiteSerializer
from .project import ProjectSerializer
from .workspace import WorkSpaceSerializer
from .project import ProjectSerializer, ProjectLiteSerializer
from .workspace import WorkspaceLiteSerializer
from plane.db.models import (
User,
Issue,
@@ -25,6 +25,7 @@ from plane.db.models import (
Module,
ModuleIssue,
IssueLink,
IssueAttachment,
)
@@ -50,8 +51,8 @@ class IssueFlatSerializer(BaseSerializer):
class IssueCreateSerializer(BaseSerializer):
state_detail = StateSerializer(read_only=True, source="state")
created_by_detail = UserLiteSerializer(read_only=True, source="created_by")
project_detail = ProjectSerializer(read_only=True, source="project")
workspace_detail = WorkSpaceSerializer(read_only=True, source="workspace")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
assignees_list = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
@@ -99,7 +100,7 @@ class IssueCreateSerializer(BaseSerializer):
project = self.context["project"]
issue = Issue.objects.create(**validated_data, project=project)
if blockers is not None:
if blockers is not None and len(blockers):
IssueBlocker.objects.bulk_create(
[
IssueBlocker(
@@ -115,7 +116,7 @@ class IssueCreateSerializer(BaseSerializer):
batch_size=10,
)
if assignees is not None:
if assignees is not None and len(assignees):
IssueAssignee.objects.bulk_create(
[
IssueAssignee(
@@ -130,8 +131,19 @@ class IssueCreateSerializer(BaseSerializer):
],
batch_size=10,
)
else:
# Then assign it to default assignee
if project.default_assignee is not None:
IssueAssignee.objects.create(
assignee=project.default_assignee,
issue=issue,
project=project,
workspace=project.workspace,
created_by=issue.created_by,
updated_by=issue.updated_by,
)
if labels is not None:
if labels is not None and len(labels):
IssueLabel.objects.bulk_create(
[
IssueLabel(
@@ -147,7 +159,7 @@ class IssueCreateSerializer(BaseSerializer):
batch_size=10,
)
if blocks is not None:
if blocks is not None and len(blocks):
IssueBlocker.objects.bulk_create(
[
IssueBlocker(
@@ -244,6 +256,7 @@ class IssueCreateSerializer(BaseSerializer):
class IssueActivitySerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
class Meta:
model = IssueActivity
@@ -253,7 +266,8 @@ class IssueActivitySerializer(BaseSerializer):
class IssueCommentSerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor")
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
project_detail = ProjectSerializer(read_only=True, source="project")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
class Meta:
model = IssueComment
@@ -296,6 +310,9 @@ class IssuePropertySerializer(BaseSerializer):
class LabelSerializer(BaseSerializer):
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
class Meta:
model = Label
fields = "__all__"
@@ -305,6 +322,16 @@ class LabelSerializer(BaseSerializer):
]
class LabelLiteSerializer(BaseSerializer):
class Meta:
model = Label
fields = [
"id",
"name",
"color",
]
class IssueLabelSerializer(BaseSerializer):
# label_details = LabelSerializer(read_only=True, source="label")
@@ -428,12 +455,29 @@ class IssueLinkSerializer(BaseSerializer):
return IssueLink.objects.create(**validated_data)
class IssueAttachmentSerializer(BaseSerializer):
class Meta:
model = IssueAttachment
fields = "__all__"
read_only_fields = [
"created_by",
"updated_by",
"created_at",
"updated_at",
"workspace",
"project",
"issue",
]
# Issue Serializer with state details
class IssueStateSerializer(BaseSerializer):
state_detail = StateSerializer(read_only=True, source="state")
project_detail = ProjectSerializer(read_only=True, source="project")
label_details = LabelSerializer(read_only=True, source="labels", many=True)
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
sub_issues_count = serializers.IntegerField(read_only=True)
bridge_id = serializers.UUIDField(read_only=True)
class Meta:
model = Issue
@@ -453,6 +497,7 @@ class IssueSerializer(BaseSerializer):
issue_cycle = IssueCycleDetailSerializer(read_only=True)
issue_module = IssueModuleDetailSerializer(read_only=True)
issue_link = IssueLinkSerializer(read_only=True, many=True)
issue_attachment = IssueAttachmentSerializer(read_only=True, many=True)
sub_issues_count = serializers.IntegerField(read_only=True)
class Meta:
@@ -466,3 +511,31 @@ class IssueSerializer(BaseSerializer):
"created_at",
"updated_at",
]
class IssueLiteSerializer(BaseSerializer):
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
state_detail = StateLiteSerializer(read_only=True, source="state")
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
sub_issues_count = serializers.IntegerField(read_only=True)
cycle_id = serializers.UUIDField(read_only=True)
module_id = serializers.UUIDField(read_only=True)
attachment_count = serializers.IntegerField(read_only=True)
link_count = serializers.IntegerField(read_only=True)
class Meta:
model = Issue
fields = "__all__"
read_only_fields = [
"start_date",
"target_date",
"completed_at",
"workspace",
"project",
"created_by",
"updated_by",
"created_at",
"updated_at",
]
+30 -3
View File
@@ -4,10 +4,18 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .user import UserLiteSerializer
from .project import ProjectSerializer
from .project import ProjectSerializer, ProjectLiteSerializer
from .workspace import WorkspaceLiteSerializer
from .issue import IssueStateSerializer
from plane.db.models import User, Module, ModuleMember, ModuleIssue, ModuleLink, ModuleFavorite
from plane.db.models import (
User,
Module,
ModuleMember,
ModuleIssue,
ModuleLink,
ModuleFavorite,
)
class ModuleWriteSerializer(BaseSerializer):
@@ -17,6 +25,9 @@ class ModuleWriteSerializer(BaseSerializer):
required=False,
)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
class Meta:
model = Module
fields = "__all__"
@@ -128,14 +139,29 @@ class ModuleLinkSerializer(BaseSerializer):
"module",
]
# Validation if url already exists
def create(self, validated_data):
if ModuleLink.objects.filter(
url=validated_data.get("url"), module_id=validated_data.get("module_id")
).exists():
raise serializers.ValidationError(
{"error": "URL already exists for this Issue"}
)
return ModuleLink.objects.create(**validated_data)
class ModuleSerializer(BaseSerializer):
project_detail = ProjectSerializer(read_only=True, source="project")
lead_detail = UserLiteSerializer(read_only=True, source="lead")
members_detail = UserLiteSerializer(read_only=True, many=True, source="members")
issue_module = ModuleIssueSerializer(read_only=True, many=True)
link_module = ModuleLinkSerializer(read_only=True, many=True)
is_favorite = serializers.BooleanField(read_only=True)
total_issues = serializers.IntegerField(read_only=True)
cancelled_issues = serializers.IntegerField(read_only=True)
completed_issues = serializers.IntegerField(read_only=True)
started_issues = serializers.IntegerField(read_only=True)
unstarted_issues = serializers.IntegerField(read_only=True)
backlog_issues = serializers.IntegerField(read_only=True)
class Meta:
model = Module
@@ -149,6 +175,7 @@ class ModuleSerializer(BaseSerializer):
"updated_at",
]
class ModuleFavoriteSerializer(BaseSerializer):
module_detail = ModuleFlatSerializer(source="module", read_only=True)
+105
View File
@@ -0,0 +1,105 @@
# Third party imports
from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .issue import IssueFlatSerializer, LabelSerializer
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 PageSerializer(BaseSerializer):
is_favorite = serializers.BooleanField(read_only=True)
label_details = LabelSerializer(read_only=True, source="labels", many=True)
labels_list = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
write_only=True,
required=False,
)
blocks = PageBlockSerializer(read_only=True, many=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
class Meta:
model = Page
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"owned_by",
]
def create(self, validated_data):
labels = validated_data.pop("labels_list", None)
project_id = self.context["project_id"]
owned_by_id = self.context["owned_by_id"]
page = Page.objects.create(
**validated_data, project_id=project_id, owned_by_id=owned_by_id
)
if labels is not None:
PageLabel.objects.bulk_create(
[
PageLabel(
label=label,
page=page,
project_id=project_id,
workspace_id=page.workspace_id,
created_by_id=page.created_by_id,
updated_by_id=page.updated_by_id,
)
for label in labels
],
batch_size=10,
)
return page
def update(self, instance, validated_data):
labels = validated_data.pop("labels_list", None)
if labels is not None:
PageLabel.objects.filter(page=instance).delete()
PageLabel.objects.bulk_create(
[
PageLabel(
label=label,
page=instance,
project_id=instance.project_id,
workspace_id=instance.workspace_id,
created_by_id=instance.created_by_id,
updated_by_id=instance.updated_by_id,
)
for label in labels
],
batch_size=10,
)
return super().update(instance, validated_data)
class PageFavoriteSerializer(BaseSerializer):
page_detail = PageSerializer(source="page", read_only=True)
class Meta:
model = PageFavorite
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"user",
]
+19 -4
View File
@@ -6,7 +6,7 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from plane.api.serializers.workspace import WorkSpaceSerializer
from plane.api.serializers.workspace import WorkSpaceSerializer, WorkspaceLiteSerializer
from plane.api.serializers.user import UserLiteSerializer
from plane.db.models import (
Project,
@@ -18,6 +18,8 @@ from plane.db.models import (
class ProjectSerializer(BaseSerializer):
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
class Meta:
model = Project
fields = "__all__"
@@ -56,12 +58,15 @@ class ProjectSerializer(BaseSerializer):
project_identifier = ProjectIdentifier.objects.filter(
name=identifier, workspace_id=instance.workspace_id
).first()
if project_identifier is None:
project = super().update(instance, validated_data)
_ = ProjectIdentifier.objects.update(name=identifier, project=project)
project_identifier = ProjectIdentifier.objects.filter(
project=project
).first()
if project_identifier is not None:
project_identifier.name = identifier
project_identifier.save()
return project
# If found check if the project_id to be updated and identifier project id is same
if project_identifier.project_id == instance.id:
# If same pass update
@@ -77,6 +82,9 @@ class ProjectDetailSerializer(BaseSerializer):
default_assignee = UserLiteSerializer(read_only=True)
project_lead = UserLiteSerializer(read_only=True)
is_favorite = serializers.BooleanField(read_only=True)
total_members = serializers.IntegerField(read_only=True)
total_cycles = serializers.IntegerField(read_only=True)
total_modules = serializers.IntegerField(read_only=True)
class Meta:
model = Project
@@ -118,3 +126,10 @@ class ProjectFavoriteSerializer(BaseSerializer):
"workspace",
"user",
]
class ProjectLiteSerializer(BaseSerializer):
class Meta:
model = Project
fields = ["id", "identifier", "name"]
read_only_fields = fields
+17
View File
@@ -1,10 +1,15 @@
# 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
fields = "__all__"
@@ -12,3 +17,15 @@ class StateSerializer(BaseSerializer):
"workspace",
"project",
]
class StateLiteSerializer(BaseSerializer):
class Meta:
model = State
fields = [
"id",
"name",
"color",
"group",
]
read_only_fields = fields
+44 -4
View File
@@ -1,14 +1,54 @@
# Third party imports
from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from plane.db.models import View
from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer
from plane.db.models import IssueView, IssueViewFavorite
from plane.utils.issue_filters import issue_filters
class ViewSerializer(BaseSerializer):
class IssueViewSerializer(BaseSerializer):
is_favorite = serializers.BooleanField(read_only=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
class Meta:
model = View
model = IssueView
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"query",
]
def create(self, validated_data):
query_params = validated_data.get("query_data", {})
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
validated_data["query"] = dict()
return IssueView.objects.create(**validated_data)
def update(self, instance, validated_data):
query_params = validated_data.get("query_data", {})
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
validated_data["query"] = dict()
validated_data["query"] = issue_filters(query_params, "PATCH")
return super().update(instance, validated_data)
class IssueViewFavoriteSerializer(BaseSerializer):
view_detail = IssueViewSerializer(source="issue_view", read_only=True)
class Meta:
model = IssueViewFavorite
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"user",
]
+30 -6
View File
@@ -5,12 +5,18 @@ from rest_framework import serializers
from .base import BaseSerializer
from .user import UserLiteSerializer
from plane.db.models import User, Workspace, WorkspaceMember, Team, TeamMember
from plane.db.models import Workspace, WorkspaceMember, Team, WorkspaceMemberInvite
from plane.db.models import (
User,
Workspace,
WorkspaceMember,
Team,
TeamMember,
WorkspaceMemberInvite,
WorkspaceTheme,
)
class WorkSpaceSerializer(BaseSerializer):
owner = UserLiteSerializer(read_only=True)
total_members = serializers.IntegerField(read_only=True)
@@ -28,7 +34,6 @@ class WorkSpaceSerializer(BaseSerializer):
class WorkSpaceMemberSerializer(BaseSerializer):
member = UserLiteSerializer(read_only=True)
workspace = WorkSpaceSerializer(read_only=True)
@@ -38,7 +43,6 @@ class WorkSpaceMemberSerializer(BaseSerializer):
class WorkSpaceMemberInviteSerializer(BaseSerializer):
workspace = WorkSpaceSerializer(read_only=True)
class Meta:
@@ -47,7 +51,6 @@ class WorkSpaceMemberInviteSerializer(BaseSerializer):
class TeamSerializer(BaseSerializer):
members_detail = UserLiteSerializer(read_only=True, source="members", many=True)
members = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
@@ -93,3 +96,24 @@ class TeamSerializer(BaseSerializer):
return super().update(instance, validated_data)
else:
return super().update(instance, validated_data)
class WorkspaceLiteSerializer(BaseSerializer):
class Meta:
model = Workspace
fields = [
"name",
"slug",
"id",
]
read_only_fields = fields
class WorkspaceThemeSerializer(BaseSerializer):
class Meta:
model = WorkspaceTheme
fields = "__all__"
read_only_fields = [
"workspace",
"actor",
]
+395 -9
View File
@@ -21,6 +21,7 @@ from plane.api.views import (
# User
UserEndpoint,
UpdateUserOnBoardedEndpoint,
UserActivityEndpoint,
## End User
# Workspaces
WorkSpaceViewSet,
@@ -38,9 +39,14 @@ from plane.api.views import (
AddTeamToProjectEndpoint,
UserLastProjectWithWorkspaceEndpoint,
UserWorkspaceInvitationEndpoint,
UserActivityGraphEndpoint,
UserIssueCompletedGraphEndpoint,
UserWorkspaceDashboardEndpoint,
WorkspaceThemeViewSet,
## End Workspaces
# File Assets
FileAssetEndpoint,
UserAssetsEndpoint,
## End File Assets
# Projects
ProjectViewSet,
@@ -61,22 +67,30 @@ from plane.api.views import (
IssueCommentViewSet,
UserWorkSpaceIssues,
BulkDeleteIssuesEndpoint,
BulkImportIssuesEndpoint,
ProjectUserViewsEndpoint,
TimeLineIssueViewSet,
IssuePropertyViewSet,
LabelViewSet,
SubIssuesEndpoint,
IssueLinkViewSet,
ModuleLinkViewSet,
BulkCreateIssueLabelsEndpoint,
IssueAttachmentEndpoint,
## End Issues
# States
StateViewSet,
## End States
# Estimates
ProjectEstimatePointEndpoint,
BulkEstimatePointEndpoint,
## End Estimates
# Shortcuts
ShortCutViewSet,
## End Shortcuts
# Views
ViewViewSet,
IssueViewViewSet,
ViewIssuesEndpoint,
IssueViewFavoriteViewSet,
## End Views
# Cycles
CycleViewSet,
@@ -86,12 +100,26 @@ from plane.api.views import (
CompletedCyclesEndpoint,
CycleFavoriteViewSet,
DraftCyclesEndpoint,
TransferCycleIssueEndpoint,
InCompleteCyclesEndpoint,
## End Cycles
# Modules
ModuleViewSet,
ModuleIssueViewSet,
ModuleFavoriteViewSet,
ModuleLinkViewSet,
BulkImportModulesEndpoint,
## End Modules
# Pages
PageViewSet,
PageBlockViewSet,
PageFavoriteViewSet,
CreateIssueFromPageBlockEndpoint,
RecentPagesEndpoint,
FavoritePagesEndpoint,
MyPagesEndpoint,
CreatedbyOtherPagesEndpoint,
## End Pages
# Api Tokens
ApiTokenEndpoint,
## End Api Tokens
@@ -102,7 +130,31 @@ from plane.api.views import (
GithubRepositorySyncViewSet,
GithubIssueSyncViewSet,
GithubCommentSyncViewSet,
BulkCreateGithubIssueSyncEndpoint,
SlackProjectSyncViewSet,
## End Integrations
# Importer
ServiceIssueImportSummaryEndpoint,
ImportServiceEndpoint,
UpdateServiceImportStatusEndpoint,
## End importer
# Search
GlobalSearchEndpoint,
IssueSearchEndpoint,
## End Search
# Gpt
GPTIntegrationEndpoint,
## End Gpt
# Release Notes
ReleaseNotesEndpoint,
## End Release Notes
# Analytics
AnalyticsEndpoint,
AnalyticViewViewset,
SavedAnalyticEndpoint,
ExportAnalyticsEndpoint,
DefaultAnalyticsEndpoint,
## End Analytics
)
@@ -153,6 +205,7 @@ urlpatterns = [
UpdateUserOnBoardedEndpoint.as_view(),
name="change-password",
),
path("users/activities/", UserActivityEndpoint.as_view(), name="user-activities"),
# user workspaces
path(
"users/me/workspaces/",
@@ -176,6 +229,23 @@ urlpatterns = [
name="workspace",
),
# user join workspace
# User Graphs
path(
"users/me/workspaces/<str:slug>/activity-graph/",
UserActivityGraphEndpoint.as_view(),
name="user-activity-graph",
),
path(
"users/me/workspaces/<str:slug>/issues-completed-graph/",
UserIssueCompletedGraphEndpoint.as_view(),
name="completed-graph",
),
path(
"users/me/workspaces/<str:slug>/dashboard/",
UserWorkspaceDashboardEndpoint.as_view(),
name="user-workspace-dashboard",
),
## User Graph
path(
"users/me/invitations/workspaces/<str:slug>/<uuid:pk>/join/",
JoinWorkspaceEndpoint.as_view(),
@@ -245,7 +315,6 @@ urlpatterns = [
"workspaces/<str:slug>/members/<uuid:pk>/",
WorkSpaceMemberViewSet.as_view(
{
"put": "update",
"patch": "partial_update",
"delete": "destroy",
"get": "retrieve",
@@ -290,6 +359,27 @@ urlpatterns = [
WorkspaceMemberUserViewsEndpoint.as_view(),
name="workspace-member-details",
),
path(
"workspaces/<str:slug>/workspace-themes/",
WorkspaceThemeViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="workspace-themes",
),
path(
"workspaces/<str:slug>/workspace-themes/<uuid:pk>/",
WorkspaceThemeViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="workspace-themes",
),
## End Workspaces ##
# Projects
path(
@@ -334,7 +424,6 @@ urlpatterns = [
ProjectMemberViewSet.as_view(
{
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
}
@@ -425,6 +514,34 @@ urlpatterns = [
name="project-state",
),
# End States ##
# Estimates
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-estimates/",
ProjectEstimatePointEndpoint.as_view(),
name="project-estimate-points",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
BulkEstimatePointEndpoint.as_view(
{
"get": "list",
"post": "create",
}
),
name="bulk-create-estimate-points",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/",
BulkEstimatePointEndpoint.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="bulk-create-estimate-points",
),
# End Estimates ##
# Shortcuts
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/shortcuts/",
@@ -452,7 +569,7 @@ urlpatterns = [
# Views
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/views/",
ViewViewSet.as_view(
IssueViewViewSet.as_view(
{
"get": "list",
"post": "create",
@@ -462,7 +579,7 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:pk>/",
ViewViewSet.as_view(
IssueViewViewSet.as_view(
{
"get": "retrieve",
"put": "update",
@@ -472,6 +589,30 @@ urlpatterns = [
),
name="project-view",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:view_id>/issues/",
ViewIssuesEndpoint.as_view(),
name="project-view-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/",
IssueViewFavoriteViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="user-favorite-view",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/<uuid:view_id>/",
IssueViewFavoriteViewSet.as_view(
{
"delete": "destroy",
}
),
name="user-favorite-view",
),
## End Views
## Cycles
path(
@@ -557,6 +698,16 @@ urlpatterns = [
),
name="user-favorite-cycle",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/transfer-issues/",
TransferCycleIssueEndpoint.as_view(),
name="transfer-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/incomplete-cycles/",
InCompleteCyclesEndpoint.as_view(),
name="transfer-issues",
),
## End Cycles
# Issue
path(
@@ -608,9 +759,20 @@ urlpatterns = [
),
name="project-issue-labels",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/bulk-create-labels/",
BulkCreateIssueLabelsEndpoint.as_view(),
name="project-bulk-labels",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/bulk-delete-issues/",
BulkDeleteIssuesEndpoint.as_view(),
name="project-issues-bulk",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/bulk-import-issues/<str:service>/",
BulkImportIssuesEndpoint.as_view(),
name="project-issues-bulk",
),
path(
"workspaces/<str:slug>/my-issues/",
@@ -644,6 +806,16 @@ urlpatterns = [
),
name="project-issue-links",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-attachments/",
IssueAttachmentEndpoint.as_view(),
name="project-issue-attachments",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-attachments/<uuid:pk>/",
IssueAttachmentEndpoint.as_view(),
name="project-issue-attachments",
),
## End Issues
## Issue Activity
path(
@@ -728,12 +900,22 @@ urlpatterns = [
path(
"workspaces/<str:slug>/file-assets/",
FileAssetEndpoint.as_view(),
name="File Assets",
name="file-assets",
),
path(
"workspaces/<str:slug>/file-assets/<uuid:pk>/",
"workspaces/file-assets/<uuid:workspace_id>/<str:asset_key>/",
FileAssetEndpoint.as_view(),
name="File Assets",
name="file-assets",
),
path(
"users/file-assets/",
UserAssetsEndpoint.as_view(),
name="user-file-assets",
),
path(
"users/file-assets/<str:asset_key>/",
UserAssetsEndpoint.as_view(),
name="user-file-assets",
),
## End File Assets
## Modules
@@ -822,7 +1004,100 @@ urlpatterns = [
),
name="user-favorite-module",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/bulk-import-modules/<str:service>/",
BulkImportModulesEndpoint.as_view(),
name="bulk-modules-create",
),
## End Modules
# 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>/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",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/recent-pages/",
RecentPagesEndpoint.as_view(),
name="recent-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/favorite-pages/",
FavoritePagesEndpoint.as_view(),
name="recent-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/my-pages/",
MyPagesEndpoint.as_view(),
name="user-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/created-by-other-pages/",
CreatedbyOtherPagesEndpoint.as_view(),
name="created-by-other-pages",
),
## End Pages
# API Tokens
path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"),
path("api-tokens/<uuid:pk>/", ApiTokenEndpoint.as_view(), name="api-tokens"),
@@ -909,6 +1184,10 @@ urlpatterns = [
}
),
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/github-repository-sync/<uuid:repo_sync_id>/bulk-create-github-issue-sync/",
BulkCreateGithubIssueSyncEndpoint.as_view(),
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/github-repository-sync/<uuid:repo_sync_id>/github-issue-sync/<uuid:pk>/",
GithubIssueSyncViewSet.as_view(
@@ -937,5 +1216,112 @@ urlpatterns = [
),
),
## End Github Integrations
# Slack Integration
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/workspace-integrations/<uuid:workspace_integration_id>/project-slack-sync/",
SlackProjectSyncViewSet.as_view(
{
"post": "create",
"get": "list",
}
),
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/workspace-integrations/<uuid:workspace_integration_id>/project-slack-sync/<uuid:pk>/",
SlackProjectSyncViewSet.as_view(
{
"delete": "destroy",
"get": "retrieve",
}
),
),
## End Slack Integration
## End Integrations
# Importer
path(
"workspaces/<str:slug>/importers/<str:service>/",
ServiceIssueImportSummaryEndpoint.as_view(),
name="importer",
),
path(
"workspaces/<str:slug>/projects/importers/<str:service>/",
ImportServiceEndpoint.as_view(),
name="importer",
),
path(
"workspaces/<str:slug>/importers/",
ImportServiceEndpoint.as_view(),
name="importer",
),
path(
"workspaces/<str:slug>/importers/<str:service>/<uuid:pk>/",
ImportServiceEndpoint.as_view(),
name="importer",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/service/<str:service>/importers/<uuid:importer_id>/",
UpdateServiceImportStatusEndpoint.as_view(),
name="importer",
),
## End Importer
# Search
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/search/",
GlobalSearchEndpoint.as_view(),
name="global-search",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/search-issues/",
IssueSearchEndpoint.as_view(),
name="project-issue-search",
),
## End Search
# Gpt
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/ai-assistant/",
GPTIntegrationEndpoint.as_view(),
name="importer",
),
## End Gpt
# Release Notes
path(
"release-notes/",
ReleaseNotesEndpoint.as_view(),
name="release-notes",
),
## End Release Notes
# Analytics
path(
"workspaces/<str:slug>/analytics/",
AnalyticsEndpoint.as_view(),
name="plane-analytics",
),
path(
"workspaces/<str:slug>/analytic-view/",
AnalyticViewViewset.as_view({"get": "list", "post": "create"}),
name="analytic-view",
),
path(
"workspaces/<str:slug>/analytic-view/<uuid:pk>/",
AnalyticViewViewset.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="analytic-view",
),
path(
"workspaces/<str:slug>/saved-analytic-view/<uuid:analytic_id>/",
SavedAnalyticEndpoint.as_view(),
name="saved-analytic-view",
),
path(
"workspaces/<str:slug>/export-analytics/",
ExportAnalyticsEndpoint.as_view(),
name="export-analytics",
),
path(
"workspaces/<str:slug>/default-analytics/",
DefaultAnalyticsEndpoint.as_view(),
name="default-analytics",
),
## End Analytics
]
+53 -2
View File
@@ -16,6 +16,7 @@ from .project import (
from .people import (
UserEndpoint,
UpdateUserOnBoardedEndpoint,
UserActivityEndpoint,
)
from .oauth import OauthEndpoint
@@ -36,10 +37,14 @@ from .workspace import (
UserLastProjectWithWorkspaceEndpoint,
WorkspaceMemberUserEndpoint,
WorkspaceMemberUserViewsEndpoint,
UserActivityGraphEndpoint,
UserIssueCompletedGraphEndpoint,
UserWorkspaceDashboardEndpoint,
WorkspaceThemeViewSet,
)
from .state import StateViewSet
from .shortcut import ShortCutViewSet
from .view import ViewViewSet
from .view import IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet
from .cycle import (
CycleViewSet,
CycleIssueViewSet,
@@ -48,8 +53,10 @@ from .cycle import (
CompletedCyclesEndpoint,
CycleFavoriteViewSet,
DraftCyclesEndpoint,
TransferCycleIssueEndpoint,
InCompleteCyclesEndpoint,
)
from .asset import FileAssetEndpoint
from .asset import FileAssetEndpoint, UserAssetsEndpoint
from .issue import (
IssueViewSet,
WorkSpaceIssuesEndpoint,
@@ -62,6 +69,8 @@ from .issue import (
UserWorkSpaceIssues,
SubIssuesEndpoint,
IssueLinkViewSet,
BulkCreateIssueLabelsEndpoint,
IssueAttachmentEndpoint,
)
from .auth_extended import (
@@ -96,4 +105,46 @@ from .integration import (
GithubRepositorySyncViewSet,
GithubCommentSyncViewSet,
GithubRepositoriesEndpoint,
BulkCreateGithubIssueSyncEndpoint,
SlackProjectSyncViewSet,
)
from .importer import (
ServiceIssueImportSummaryEndpoint,
ImportServiceEndpoint,
UpdateServiceImportStatusEndpoint,
BulkImportIssuesEndpoint,
BulkImportModulesEndpoint,
)
from .page import (
PageViewSet,
PageBlockViewSet,
PageFavoriteViewSet,
CreateIssueFromPageBlockEndpoint,
RecentPagesEndpoint,
FavoritePagesEndpoint,
MyPagesEndpoint,
CreatedbyOtherPagesEndpoint,
)
from .search import GlobalSearchEndpoint, IssueSearchEndpoint
from .gpt import GPTIntegrationEndpoint
from .estimate import (
ProjectEstimatePointEndpoint,
BulkEstimatePointEndpoint,
)
from .release import ReleaseNotesEndpoint
from .analytic import (
AnalyticsEndpoint,
AnalyticViewViewset,
SavedAnalyticEndpoint,
ExportAnalyticsEndpoint,
DefaultAnalyticsEndpoint,
)
+295
View File
@@ -0,0 +1,295 @@
# Django imports
from django.db.models import (
Count,
Sum,
F,
)
from django.db.models.functions import ExtractMonth
# Third party imports
from rest_framework import status
from rest_framework.response import Response
from sentry_sdk import capture_exception
# Module imports
from plane.api.views import BaseAPIView, BaseViewSet
from plane.api.permissions import WorkSpaceAdminPermission
from plane.db.models import Issue, AnalyticView, Workspace, State, Label
from plane.api.serializers import AnalyticViewSerializer
from plane.utils.analytics_plot import build_graph_plot
from plane.bgtasks.analytic_plot_export import analytic_export_task
from plane.utils.issue_filters import issue_filters
class AnalyticsEndpoint(BaseAPIView):
permission_classes = [
WorkSpaceAdminPermission,
]
def get(self, request, slug):
try:
x_axis = request.GET.get("x_axis", False)
y_axis = request.GET.get("y_axis", False)
if not x_axis or not y_axis:
return Response(
{"error": "x-axis and y-axis dimensions are required"},
status=status.HTTP_400_BAD_REQUEST,
)
segment = request.GET.get("segment", False)
filters = issue_filters(request.GET, "GET")
queryset = Issue.objects.filter(workspace__slug=slug, **filters)
total_issues = queryset.count()
distribution = build_graph_plot(
queryset=queryset, x_axis=x_axis, y_axis=y_axis, segment=segment
)
colors = dict()
if x_axis in ["state__name", "state__group"] or segment in [
"state__name",
"state__group",
]:
if x_axis in ["state__name", "state__group"]:
key = "name" if x_axis == "state__name" else "group"
else:
key = "name" if segment == "state__name" else "group"
colors = (
State.objects.filter(
workspace__slug=slug, project_id__in=filters.get("project__in")
).values(key, "color")
if filters.get("project__in", False)
else State.objects.filter(workspace__slug=slug).values(key, "color")
)
if x_axis in ["labels__name"] or segment in ["labels__name"]:
colors = (
Label.objects.filter(
workspace__slug=slug, project_id__in=filters.get("project__in")
).values("name", "color")
if filters.get("project__in", False)
else Label.objects.filter(workspace__slug=slug).values(
"name", "color"
)
)
assignee_details = {}
if x_axis in ["assignees__email"] or segment in ["assignees__email"]:
assignee_details = (
Issue.objects.filter(workspace__slug=slug, **filters, assignees__avatar__isnull=False)
.order_by("assignees__id")
.distinct("assignees__id")
.values("assignees__avatar", "assignees__email", "assignees__first_name", "assignees__last_name")
)
return Response(
{
"total": total_issues,
"distribution": distribution,
"extras": {"colors": colors, "assignee_details": assignee_details},
},
status=status.HTTP_200_OK,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class AnalyticViewViewset(BaseViewSet):
permission_classes = [
WorkSpaceAdminPermission,
]
model = AnalyticView
serializer_class = AnalyticViewSerializer
def perform_create(self, serializer):
workspace = Workspace.objects.get(slug=self.kwargs.get("slug"))
serializer.save(workspace_id=workspace.id)
def get_queryset(self):
return self.filter_queryset(
super().get_queryset().filter(workspace__slug=self.kwargs.get("slug"))
)
class SavedAnalyticEndpoint(BaseAPIView):
permission_classes = [
WorkSpaceAdminPermission,
]
def get(self, request, slug, analytic_id):
try:
analytic_view = AnalyticView.objects.get(
pk=analytic_id, workspace__slug=slug
)
filter = analytic_view.query
queryset = Issue.objects.filter(**filter)
x_axis = analytic_view.query_dict.get("x_axis", False)
y_axis = analytic_view.query_dict.get("y_axis", False)
if not x_axis or not y_axis:
return Response(
{"error": "x-axis and y-axis dimensions are required"},
status=status.HTTP_400_BAD_REQUEST,
)
segment = request.GET.get("segment", False)
distribution = build_graph_plot(
queryset=queryset, x_axis=x_axis, y_axis=y_axis, segment=segment
)
total_issues = queryset.count()
return Response(
{"total": total_issues, "distribution": distribution},
status=status.HTTP_200_OK,
)
except AnalyticView.DoesNotExist:
return Response(
{"error": "Analytic View Does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class ExportAnalyticsEndpoint(BaseAPIView):
permission_classes = [
WorkSpaceAdminPermission,
]
def post(self, request, slug):
try:
x_axis = request.data.get("x_axis", False)
y_axis = request.data.get("y_axis", False)
if not x_axis or not y_axis:
return Response(
{"error": "x-axis and y-axis dimensions are required"},
status=status.HTTP_400_BAD_REQUEST,
)
analytic_export_task.delay(
email=request.user.email, data=request.data, slug=slug
)
return Response(
{
"message": f"Once the export is ready it will be emailed to you at {str(request.user.email)}"
},
status=status.HTTP_200_OK,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class DefaultAnalyticsEndpoint(BaseAPIView):
permission_classes = [
WorkSpaceAdminPermission,
]
def get(self, request, slug):
try:
filters = issue_filters(request.GET, "GET")
queryset = Issue.objects.filter(workspace__slug=slug, **filters)
total_issues = queryset.count()
total_issues_classified = (
queryset.annotate(state_group=F("state__group"))
.values("state_group")
.annotate(state_count=Count("state_group"))
.order_by("state_group")
)
open_issues = queryset.filter(
state__group__in=["backlog", "unstarted", "started"]
).count()
open_issues_classified = (
queryset.filter(state__group__in=["backlog", "unstarted", "started"])
.annotate(state_group=F("state__group"))
.values("state_group")
.annotate(state_count=Count("state_group"))
.order_by("state_group")
)
issue_completed_month_wise = (
queryset.filter(completed_at__isnull=False)
.annotate(month=ExtractMonth("completed_at"))
.values("month")
.annotate(count=Count("*"))
.order_by("month")
)
most_issue_created_user = (
queryset.exclude(created_by=None)
.values("created_by__first_name", "created_by__last_name", "created_by__avatar", "created_by__email")
.annotate(count=Count("id"))
.order_by("-count")
)[:5]
most_issue_closed_user = (
queryset.filter(completed_at__isnull=False, assignees__isnull=False)
.values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__email")
.annotate(count=Count("id"))
.order_by("-count")
)[:5]
pending_issue_user = (
queryset.filter(completed_at__isnull=True)
.values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__email")
.annotate(count=Count("id"))
.order_by("-count")
)
open_estimate_sum = (
queryset.filter(
state__group__in=["backlog", "unstarted", "started"]
).aggregate(open_estimate_sum=Sum("estimate_point"))
)["open_estimate_sum"]
print(open_estimate_sum)
total_estimate_sum = queryset.aggregate(
total_estimate_sum=Sum("estimate_point")
)["total_estimate_sum"]
return Response(
{
"total_issues": total_issues,
"total_issues_classified": total_issues_classified,
"open_issues": open_issues,
"open_issues_classified": open_issues_classified,
"issue_completed_month_wise": issue_completed_month_wise,
"most_issue_created_user": most_issue_created_user,
"most_issue_closed_user": most_issue_closed_user,
"pending_issue_user": pending_issue_user,
"open_estimate_sum": open_estimate_sum,
"total_estimate_sum": total_estimate_sum,
},
status=status.HTTP_200_OK,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
+53 -4
View File
@@ -17,8 +17,9 @@ class FileAssetEndpoint(BaseAPIView):
A viewset for viewing and editing task instances.
"""
def get(self, request, slug):
files = FileAsset.objects.filter(workspace__slug=slug)
def get(self, request, workspace_id, asset_key):
asset_key = str(workspace_id) + "/" + asset_key
files = FileAsset.objects.filter(asset=asset_key)
serializer = FileAssetSerializer(files, context={"request": request}, many=True)
return Response(serializer.data)
@@ -42,9 +43,57 @@ class FileAssetEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST,
)
def delete(self, request, slug, pk):
def delete(self, request, workspace_id, asset_key):
try:
file_asset = FileAsset.objects.get(pk=pk, workspace__slug=slug)
asset_key = str(workspace_id) + "/" + asset_key
file_asset = FileAsset.objects.get(asset=asset_key)
# Delete the file from storage
file_asset.asset.delete(save=False)
# Delete the file object
file_asset.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except FileAsset.DoesNotExist:
return Response(
{"error": "File Asset doesn't exist"}, status=status.HTTP_404_NOT_FOUND
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class UserAssetsEndpoint(BaseAPIView):
parser_classes = (MultiPartParser, FormParser)
def get(self, request, asset_key):
try:
files = FileAsset.objects.filter(asset=asset_key, created_by=request.user)
serializer = FileAssetSerializer(files, context={"request": request})
return Response(serializer.data)
except FileAsset.DoesNotExist:
return Response(
{"error": "File Asset does not exist"}, status=status.HTTP_404_NOT_FOUND
)
def post(self, request):
try:
serializer = FileAssetSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def delete(self, request, asset_key):
try:
file_asset = FileAsset.objects.get(asset=asset_key, created_by=request.user)
# Delete the file from storage
file_asset.asset.delete(save=False)
# Delete the file object
+90 -1
View File
@@ -3,6 +3,7 @@ import uuid
import random
import string
import json
import requests
# Django imports
from django.utils import timezone
@@ -85,6 +86,28 @@ class SignInEndpoint(BaseAPIView):
"user": serialized_user,
}
# Send Analytics
if settings.ANALYTICS_BASE_API:
_ = requests.post(
settings.ANALYTICS_BASE_API,
headers={
"Content-Type": "application/json",
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
},
json={
"event_id": uuid.uuid4().hex,
"event_data": {
"medium": "email",
},
"user": {"email": email, "id": str(user.id)},
"device_ctx": {
"ip": request.META.get("REMOTE_ADDR"),
"user_agent": request.META.get("HTTP_USER_AGENT"),
},
"event_type": "SIGN_UP",
},
)
return Response(data, status=status.HTTP_200_OK)
# Sign in Process
else:
@@ -114,7 +137,27 @@ class SignInEndpoint(BaseAPIView):
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,
@@ -268,6 +311,29 @@ class MagicSignInEndpoint(BaseAPIView):
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,
@@ -275,6 +341,29 @@ class MagicSignInEndpoint(BaseAPIView):
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()
+2 -2
View File
@@ -10,7 +10,7 @@ from rest_framework.views import APIView
from rest_framework.filters import SearchFilter
from rest_framework.permissions import IsAuthenticated
from rest_framework.exceptions import NotFound
from sentry_sdk import capture_exception
from django_filters.rest_framework import DjangoFilterBackend
# Module imports
@@ -39,7 +39,7 @@ class BaseViewSet(ModelViewSet, BasePaginator):
try:
return self.model.objects.all()
except Exception as e:
print(e)
capture_exception(e)
raise APIException("Please check the view", status.HTTP_400_BAD_REQUEST)
def dispatch(self, request, *args, **kwargs):
+588 -66
View File
@@ -3,9 +3,21 @@ import json
# Django imports
from django.db import IntegrityError
from django.db.models import OuterRef, Func, F, Q, Exists, OuterRef
from django.db.models import (
OuterRef,
Func,
F,
Q,
Exists,
OuterRef,
Count,
Prefetch,
Sum,
)
from django.core import serializers
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
# Third party imports
from rest_framework.response import Response
@@ -18,11 +30,21 @@ from plane.api.serializers import (
CycleSerializer,
CycleIssueSerializer,
CycleFavoriteSerializer,
IssueStateSerializer,
)
from plane.api.permissions import ProjectEntityPermission
from plane.db.models import Cycle, CycleIssue, Issue, CycleFavorite
from plane.db.models import (
User,
Cycle,
CycleIssue,
Issue,
CycleFavorite,
IssueLink,
IssueAttachment,
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.grouper import group_results
from plane.utils.issue_filters import issue_filters
class CycleViewSet(BaseViewSet):
@@ -37,7 +59,35 @@ class CycleViewSet(BaseViewSet):
project_id=self.kwargs.get("project_id"), owned_by=self.request.user
)
def perform_destroy(self, instance):
cycle_issues = list(
CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
"issue", flat=True
)
)
issue_activity.delay(
type="cycle.activity.deleted",
requested_data=json.dumps(
{
"cycle_id": str(self.kwargs.get("pk")),
"issues": [str(issue_id) for issue_id in cycle_issues],
}
),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
)
return super().perform_destroy(instance)
def get_queryset(self):
subquery = CycleFavorite.objects.filter(
user=self.request.user,
cycle_id=OuterRef("pk"),
project_id=self.kwargs.get("project_id"),
workspace__slug=self.kwargs.get("slug"),
)
return self.filter_queryset(
super()
.get_queryset()
@@ -47,26 +97,61 @@ class CycleViewSet(BaseViewSet):
.select_related("project")
.select_related("workspace")
.select_related("owned_by")
.annotate(is_favorite=Exists(subquery))
.annotate(total_issues=Count("issue_cycle"))
.annotate(
completed_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="completed"),
)
)
.annotate(
cancelled_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="cancelled"),
)
)
.annotate(
started_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="started"),
)
)
.annotate(
unstarted_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="unstarted"),
)
)
.annotate(
backlog_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="backlog"),
)
)
.annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
.annotate(
completed_estimates=Sum(
"issue_cycle__issue__estimate_point",
filter=Q(issue_cycle__issue__state__group="completed"),
)
)
.annotate(
started_estimates=Sum(
"issue_cycle__issue__estimate_point",
filter=Q(issue_cycle__issue__state__group="started"),
)
)
.prefetch_related(
Prefetch(
"issue_cycle__issue__assignees",
queryset=User.objects.only("avatar", "first_name", "id").distinct(),
)
)
.order_by("-is_favorite", "name")
.distinct()
)
def list(self, request, slug, project_id):
try:
subquery = CycleFavorite.objects.filter(
user=self.request.user,
cycle_id=OuterRef("pk"),
project_id=project_id,
workspace__slug=slug,
)
cycles = self.get_queryset().annotate(is_favorite=Exists(subquery))
return Response(CycleSerializer(cycles, many=True).data)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def create(self, request, slug, project_id):
try:
if (
@@ -98,6 +183,36 @@ class CycleViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
def partial_update(self, request, slug, project_id, pk):
try:
cycle = Cycle.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
if cycle.end_date is not None and cycle.end_date < timezone.now().date():
return Response(
{
"error": "The Cycle has already been completed so it cannot be edited"
},
status=status.HTTP_400_BAD_REQUEST,
)
serializer = CycleSerializer(cycle, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Cycle.DoesNotExist:
return Response(
{"error": "Cycle does not exist"}, status=status.HTTP_400_BAD_REQUEST
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class CycleIssueViewSet(BaseViewSet):
serializer_class = CycleIssueSerializer
@@ -118,6 +233,22 @@ class CycleIssueViewSet(BaseViewSet):
cycle_id=self.kwargs.get("cycle_id"),
)
def perform_destroy(self, instance):
issue_activity.delay(
type="cycle.activity.deleted",
requested_data=json.dumps(
{
"cycle_id": str(self.kwargs.get("cycle_id")),
"issues": [str(instance.issue_id)],
}
),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
)
return super().perform_destroy(instance)
def get_queryset(self):
return self.filter_queryset(
super()
@@ -140,22 +271,57 @@ class CycleIssueViewSet(BaseViewSet):
.distinct()
)
@method_decorator(gzip_page)
def list(self, request, slug, project_id, cycle_id):
try:
order_by = request.GET.get("order_by", "created_at")
queryset = self.get_queryset().order_by(f"issue__{order_by}")
group_by = request.GET.get("group_by", False)
filters = issue_filters(request.query_params, "GET")
issues = (
Issue.objects.filter(issue_cycle__cycle_id=cycle_id)
.annotate(
sub_issues_count=Issue.objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(bridge_id=F("issue_cycle__id"))
.filter(project_id=project_id)
.filter(workspace__slug=slug)
.select_related("project")
.select_related("workspace")
.select_related("state")
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
.order_by(order_by)
.filter(**filters)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=IssueAttachment.objects.filter(
issue=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
cycle_issues = CycleIssueSerializer(queryset, many=True).data
issues_data = IssueStateSerializer(issues, many=True).data
if group_by:
return Response(
group_results(cycle_issues, f"issue_detail.{group_by}"),
group_results(issues_data, group_by),
status=status.HTTP_200_OK,
)
return Response(
cycle_issues,
issues_data,
status=status.HTTP_200_OK,
)
except Exception as e:
@@ -178,11 +344,19 @@ class CycleIssueViewSet(BaseViewSet):
workspace__slug=slug, project_id=project_id, pk=cycle_id
)
if cycle.end_date is not None and cycle.end_date < timezone.now().date():
return Response(
{
"error": "The Cycle has already been completed so no new issues can be added"
},
status=status.HTTP_400_BAD_REQUEST,
)
# Get all CycleIssues already created
cycle_issues = list(CycleIssue.objects.filter(issue_id__in=issues))
records_to_update = []
update_cycle_issue_activity = []
record_to_create = []
records_to_update = []
for issue in issues:
cycle_issue = [
@@ -227,21 +401,19 @@ class CycleIssueViewSet(BaseViewSet):
# Capture Issue Activity
issue_activity.delay(
{
"type": "issue.activity",
"requested_data": json.dumps({"cycles_list": issues}),
"actor_id": str(self.request.user.id),
"issue_id": str(self.kwargs.get("pk", None)),
"project_id": str(self.kwargs.get("project_id", None)),
"current_instance": json.dumps(
{
"updated_cycle_issues": update_cycle_issue_activity,
"created_cycle_issues": serializers.serialize(
"json", record_to_create
),
}
),
},
type="cycle.activity.created",
requested_data=json.dumps({"cycles_list": issues}),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
{
"updated_cycle_issues": update_cycle_issue_activity,
"created_cycle_issues": serializers.serialize(
"json", record_to_create
),
}
),
)
# Return all Cycle Issues
@@ -263,17 +435,30 @@ class CycleIssueViewSet(BaseViewSet):
class CycleDateCheckEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def post(self, request, slug, project_id):
try:
start_date = request.data.get("start_date")
end_date = request.data.get("end_date")
start_date = request.data.get("start_date", False)
end_date = request.data.get("end_date", False)
cycle_id = request.data.get("cycle_id")
if not start_date or not end_date:
return Response(
{"error": "Start date and end date both are required"},
status=status.HTTP_400_BAD_REQUEST,
)
cycles = Cycle.objects.filter(
Q(start_date__lte=start_date, end_date__gte=start_date)
| Q(start_date__gte=end_date, end_date__lte=end_date),
workspace__slug=slug,
project_id=project_id,
)
Q(workspace__slug=slug)
& Q(project_id=project_id)
& (
Q(start_date__lte=start_date, end_date__gte=start_date)
| Q(start_date__lte=end_date, end_date__gte=end_date)
| Q(start_date__gte=start_date, end_date__lte=end_date)
)
).exclude(pk=cycle_id)
if cycles.exists():
return Response(
@@ -294,6 +479,10 @@ class CycleDateCheckEndpoint(BaseAPIView):
class CurrentUpcomingCyclesEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def get(self, request, slug, project_id):
try:
subquery = CycleFavorite.objects.filter(
@@ -302,18 +491,136 @@ class CurrentUpcomingCyclesEndpoint(BaseAPIView):
project_id=project_id,
workspace__slug=slug,
)
current_cycle = Cycle.objects.filter(
workspace__slug=slug,
project_id=project_id,
start_date__lte=timezone.now(),
end_date__gte=timezone.now(),
).annotate(is_favorite=Exists(subquery))
current_cycle = (
Cycle.objects.filter(
workspace__slug=slug,
project_id=project_id,
start_date__lte=timezone.now(),
end_date__gte=timezone.now(),
)
.select_related("project")
.select_related("workspace")
.select_related("owned_by")
.annotate(is_favorite=Exists(subquery))
.annotate(total_issues=Count("issue_cycle"))
.annotate(
completed_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="completed"),
)
)
.annotate(
cancelled_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="cancelled"),
)
)
.annotate(
started_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="started"),
)
)
.annotate(
unstarted_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="unstarted"),
)
)
.annotate(
backlog_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="backlog"),
)
)
.annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
.annotate(
completed_estimates=Sum(
"issue_cycle__issue__estimate_point",
filter=Q(issue_cycle__issue__state__group="completed"),
)
)
.annotate(
started_estimates=Sum(
"issue_cycle__issue__estimate_point",
filter=Q(issue_cycle__issue__state__group="started"),
)
)
.prefetch_related(
Prefetch(
"issue_cycle__issue__assignees",
queryset=User.objects.only(
"avatar", "first_name", "id"
).distinct(),
)
)
.order_by("name", "-is_favorite")
)
upcoming_cycle = Cycle.objects.filter(
workspace__slug=slug,
project_id=project_id,
start_date__gt=timezone.now(),
).annotate(is_favorite=Exists(subquery))
upcoming_cycle = (
Cycle.objects.filter(
workspace__slug=slug,
project_id=project_id,
start_date__gt=timezone.now(),
)
.select_related("project")
.select_related("workspace")
.select_related("owned_by")
.annotate(is_favorite=Exists(subquery))
.annotate(total_issues=Count("issue_cycle"))
.annotate(
completed_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="completed"),
)
)
.annotate(
cancelled_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="cancelled"),
)
)
.annotate(
started_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="started"),
)
)
.annotate(
unstarted_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="unstarted"),
)
)
.annotate(
backlog_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="backlog"),
)
)
.annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
.annotate(
completed_estimates=Sum(
"issue_cycle__issue__estimate_point",
filter=Q(issue_cycle__issue__state__group="completed"),
)
)
.annotate(
started_estimates=Sum(
"issue_cycle__issue__estimate_point",
filter=Q(issue_cycle__issue__state__group="started"),
)
)
.prefetch_related(
Prefetch(
"issue_cycle__issue__assignees",
queryset=User.objects.only(
"avatar", "first_name", "id"
).distinct(),
)
)
.order_by("name", "-is_favorite")
)
return Response(
{
@@ -332,6 +639,10 @@ class CurrentUpcomingCyclesEndpoint(BaseAPIView):
class CompletedCyclesEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def get(self, request, slug, project_id):
try:
subquery = CycleFavorite.objects.filter(
@@ -340,11 +651,70 @@ class CompletedCyclesEndpoint(BaseAPIView):
project_id=project_id,
workspace__slug=slug,
)
completed_cycles = Cycle.objects.filter(
workspace__slug=slug,
project_id=project_id,
end_date__lt=timezone.now(),
).annotate(is_favorite=Exists(subquery))
completed_cycles = (
Cycle.objects.filter(
workspace__slug=slug,
project_id=project_id,
end_date__lt=timezone.now(),
)
.select_related("project")
.select_related("workspace")
.select_related("owned_by")
.annotate(is_favorite=Exists(subquery))
.annotate(total_issues=Count("issue_cycle"))
.annotate(
completed_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="completed"),
)
)
.annotate(
cancelled_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="cancelled"),
)
)
.annotate(
started_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="started"),
)
)
.annotate(
unstarted_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="unstarted"),
)
)
.annotate(
backlog_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="backlog"),
)
)
.annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
.annotate(
completed_estimates=Sum(
"issue_cycle__issue__estimate_point",
filter=Q(issue_cycle__issue__state__group="completed"),
)
)
.annotate(
started_estimates=Sum(
"issue_cycle__issue__estimate_point",
filter=Q(issue_cycle__issue__state__group="started"),
)
)
.prefetch_related(
Prefetch(
"issue_cycle__issue__assignees",
queryset=User.objects.only(
"avatar", "first_name", "id"
).distinct(),
)
)
.order_by("name", "-is_favorite")
)
return Response(
{
@@ -364,13 +734,82 @@ class CompletedCyclesEndpoint(BaseAPIView):
class DraftCyclesEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def get(self, request, slug, project_id):
try:
draft_cycles = Cycle.objects.filter(
workspace__slug=slug,
subquery = CycleFavorite.objects.filter(
user=self.request.user,
cycle_id=OuterRef("pk"),
project_id=project_id,
end_date=None,
start_date=None,
workspace__slug=slug,
)
draft_cycles = (
Cycle.objects.filter(
workspace__slug=slug,
project_id=project_id,
end_date=None,
start_date=None,
)
.select_related("project")
.select_related("workspace")
.select_related("owned_by")
.annotate(is_favorite=Exists(subquery))
.annotate(total_issues=Count("issue_cycle"))
.annotate(
completed_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="completed"),
)
)
.annotate(
cancelled_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="cancelled"),
)
)
.annotate(
started_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="started"),
)
)
.annotate(
unstarted_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="unstarted"),
)
)
.annotate(
backlog_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="backlog"),
)
)
.annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
.annotate(
completed_estimates=Sum(
"issue_cycle__issue__estimate_point",
filter=Q(issue_cycle__issue__state__group="completed"),
)
)
.annotate(
started_estimates=Sum(
"issue_cycle__issue__estimate_point",
filter=Q(issue_cycle__issue__state__group="started"),
)
)
.prefetch_related(
Prefetch(
"issue_cycle__issue__assignees",
queryset=User.objects.only(
"avatar", "first_name", "id"
).distinct(),
)
)
.order_by("name", "-is_favorite")
)
return Response(
@@ -386,6 +825,10 @@ class DraftCyclesEndpoint(BaseAPIView):
class CycleFavoriteViewSet(BaseViewSet):
permission_classes = [
ProjectEntityPermission,
]
serializer_class = CycleFavoriteSerializer
model = CycleFavorite
@@ -445,3 +888,82 @@ class CycleFavoriteViewSet(BaseViewSet):
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class TransferCycleIssueEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def post(self, request, slug, project_id, cycle_id):
try:
new_cycle_id = request.data.get("new_cycle_id", False)
if not new_cycle_id:
return Response(
{"error": "New Cycle Id is required"},
status=status.HTTP_400_BAD_REQUEST,
)
new_cycle = Cycle.objects.get(
workspace__slug=slug, project_id=project_id, pk=new_cycle_id
)
if (
new_cycle.end_date is not None
and new_cycle.end_date < timezone.now().date()
):
return Response(
{
"error": "The cycle where the issues are transferred is already completed"
},
status=status.HTTP_400_BAD_REQUEST,
)
cycle_issues = CycleIssue.objects.filter(
cycle_id=cycle_id,
project_id=project_id,
workspace__slug=slug,
issue__state__group__in=["backlog", "unstarted", "started"],
)
updated_cycles = []
for cycle_issue in cycle_issues:
cycle_issue.cycle_id = new_cycle_id
updated_cycles.append(cycle_issue)
cycle_issues = CycleIssue.objects.bulk_update(
updated_cycles, ["cycle_id"], batch_size=100
)
return Response({"message": "Success"}, status=status.HTTP_200_OK)
except Cycle.DoesNotExist:
return Response(
{"error": "New Cycle Does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class InCompleteCyclesEndpoint(BaseAPIView):
def get(self, request, slug, project_id):
try:
cycles = Cycle.objects.filter(
Q(end_date__gte=timezone.now().date()) | Q(end_date__isnull=True),
workspace__slug=slug,
project_id=project_id,
).select_related("owned_by")
serializer = CycleSerializer(cycles, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
+253
View File
@@ -0,0 +1,253 @@
# Django imports
from django.db import IntegrityError
# Third party imports
from rest_framework.response import Response
from rest_framework import status
from sentry_sdk import capture_exception
# Module imports
from .base import BaseViewSet, BaseAPIView
from plane.api.permissions import ProjectEntityPermission
from plane.db.models import Project, Estimate, EstimatePoint
from plane.api.serializers import (
EstimateSerializer,
EstimatePointSerializer,
EstimateReadSerializer,
)
class ProjectEstimatePointEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def get(self, request, slug, project_id):
try:
project = Project.objects.get(workspace__slug=slug, pk=project_id)
if project.estimate_id is not None:
estimate_points = EstimatePoint.objects.filter(
estimate_id=project.estimate_id,
project_id=project_id,
workspace__slug=slug,
)
serializer = EstimatePointSerializer(estimate_points, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response([], status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class BulkEstimatePointEndpoint(BaseViewSet):
permission_classes = [
ProjectEntityPermission,
]
model = Estimate
serializer_class = EstimateSerializer
def list(self, request, slug, project_id):
try:
estimates = Estimate.objects.filter(
workspace__slug=slug, project_id=project_id
).prefetch_related("points").select_related("workspace", "project")
serializer = EstimateReadSerializer(estimates, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def create(self, request, slug, project_id):
try:
if not request.data.get("estimate", False):
return Response(
{"error": "Estimate is required"},
status=status.HTTP_400_BAD_REQUEST,
)
estimate_points = request.data.get("estimate_points", [])
if not len(estimate_points) or len(estimate_points) > 8:
return Response(
{"error": "Estimate points are required"},
status=status.HTTP_400_BAD_REQUEST,
)
estimate_serializer = EstimateSerializer(data=request.data.get("estimate"))
if not estimate_serializer.is_valid():
return Response(
estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
try:
estimate = estimate_serializer.save(project_id=project_id)
except IntegrityError:
return Response(
{"errror": "Estimate with the name already exists"},
status=status.HTTP_400_BAD_REQUEST,
)
estimate_points = EstimatePoint.objects.bulk_create(
[
EstimatePoint(
estimate=estimate,
key=estimate_point.get("key", 0),
value=estimate_point.get("value", ""),
description=estimate_point.get("description", ""),
project_id=project_id,
workspace_id=estimate.workspace_id,
created_by=request.user,
updated_by=request.user,
)
for estimate_point in estimate_points
],
batch_size=10,
ignore_conflicts=True,
)
estimate_point_serializer = EstimatePointSerializer(
estimate_points, many=True
)
return Response(
{
"estimate": estimate_serializer.data,
"estimate_points": estimate_point_serializer.data,
},
status=status.HTTP_200_OK,
)
except Estimate.DoesNotExist:
return Response(
{"error": "Estimate does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def retrieve(self, request, slug, project_id, estimate_id):
try:
estimate = Estimate.objects.get(
pk=estimate_id, workspace__slug=slug, project_id=project_id
)
serializer = EstimateReadSerializer(estimate)
return Response(
serializer.data,
status=status.HTTP_200_OK,
)
except Estimate.DoesNotExist:
return Response(
{"error": "Estimate does not exist"}, status=status.HTTP_400_BAD_REQUEST
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def partial_update(self, request, slug, project_id, estimate_id):
try:
if not request.data.get("estimate", False):
return Response(
{"error": "Estimate is required"},
status=status.HTTP_400_BAD_REQUEST,
)
if not len(request.data.get("estimate_points", [])):
return Response(
{"error": "Estimate points are required"},
status=status.HTTP_400_BAD_REQUEST,
)
estimate = Estimate.objects.get(pk=estimate_id)
estimate_serializer = EstimateSerializer(
estimate, data=request.data.get("estimate"), partial=True
)
if not estimate_serializer.is_valid():
return Response(
estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
try:
estimate = estimate_serializer.save()
except IntegrityError:
return Response(
{"errror": "Estimate with the name already exists"},
status=status.HTTP_400_BAD_REQUEST,
)
estimate_points_data = request.data.get("estimate_points", [])
estimate_points = EstimatePoint.objects.filter(
pk__in=[
estimate_point.get("id") for estimate_point in estimate_points_data
],
workspace__slug=slug,
project_id=project_id,
estimate_id=estimate_id,
)
updated_estimate_points = []
for estimate_point in estimate_points:
# Find the data for that estimate point
estimate_point_data = [
point
for point in estimate_points_data
if point.get("id") == str(estimate_point.id)
]
if len(estimate_point_data):
estimate_point.value = estimate_point_data[0].get(
"value", estimate_point.value
)
updated_estimate_points.append(estimate_point)
try:
EstimatePoint.objects.bulk_update(
updated_estimate_points, ["value"], batch_size=10,
)
except IntegrityError as e:
return Response(
{"error": "Values need to be unique for each key"},
status=status.HTTP_400_BAD_REQUEST,
)
estimate_point_serializer = EstimatePointSerializer(estimate_points, many=True)
return Response(
{
"estimate": estimate_serializer.data,
"estimate_points": estimate_point_serializer.data,
},
status=status.HTTP_200_OK,
)
except Estimate.DoesNotExist:
return Response(
{"error": "Estimate does not exist"}, status=status.HTTP_400_BAD_REQUEST
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def destroy(self, request, slug, project_id, estimate_id):
try:
estimate = Estimate.objects.get(
pk=estimate_id, workspace__slug=slug, project_id=project_id
)
estimate.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
+101
View File
@@ -0,0 +1,101 @@
# Python imports
import requests
# Third party imports
from rest_framework.response import Response
from rest_framework import status
import openai
from sentry_sdk import capture_exception
# Django imports
from django.conf import settings
# Module imports
from .base import BaseAPIView
from plane.api.permissions import ProjectEntityPermission
from plane.db.models import Workspace, Project
from plane.api.serializers import ProjectLiteSerializer, WorkspaceLiteSerializer
class GPTIntegrationEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def post(self, request, slug, project_id):
try:
if not settings.OPENAI_API_KEY or not settings.GPT_ENGINE:
return Response(
{"error": "OpenAI API key and engine is required"},
status=status.HTTP_400_BAD_REQUEST,
)
count = 0
# If logger is enabled check for request limit
if settings.LOGGER_BASE_URL:
try:
headers = {
"Content-Type": "application/json",
}
response = requests.post(
settings.LOGGER_BASE_URL,
json={"user_id": str(request.user.id)},
headers=headers,
)
count = response.json().get("count", 0)
if not response.json().get("success", False):
return Response(
{
"error": "You have surpassed the monthly limit for AI assistance"
},
status=status.HTTP_429_TOO_MANY_REQUESTS,
)
except Exception as e:
capture_exception(e)
prompt = request.data.get("prompt", False)
task = request.data.get("task", False)
if not task:
return Response(
{"error": "Task is required"}, status=status.HTTP_400_BAD_REQUEST
)
final_text = task + "\n" + prompt
openai.api_key = settings.OPENAI_API_KEY
response = openai.Completion.create(
engine=settings.GPT_ENGINE,
prompt=final_text,
temperature=0.7,
max_tokens=1024,
)
workspace = Workspace.objects.get(slug=slug)
project = Project.objects.get(pk=project_id)
text = response.choices[0].text.strip()
text_html = text.replace("\n", "<br/>")
return Response(
{
"response": text,
"response_html": text_html,
"count": count,
"project_detail": ProjectLiteSerializer(project).data,
"workspace_detail": WorkspaceLiteSerializer(workspace).data,
},
status=status.HTTP_200_OK,
)
except (Workspace.DoesNotExist, Project.DoesNotExist) as e:
return Response(
{"error": "Workspace or Project Does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
+580
View File
@@ -0,0 +1,580 @@
# Python imports
import uuid
# Third party imports
from rest_framework import status
from rest_framework.response import Response
from sentry_sdk import capture_exception
# Django imports
from django.db.models import Max
# Module imports
from plane.api.views import BaseAPIView
from plane.db.models import (
WorkspaceIntegration,
Importer,
APIToken,
Project,
State,
IssueSequence,
Issue,
IssueActivity,
IssueComment,
IssueLink,
IssueLabel,
Workspace,
IssueAssignee,
Module,
ModuleLink,
ModuleIssue,
Label,
)
from plane.api.serializers import (
ImporterSerializer,
IssueFlatSerializer,
ModuleSerializer,
)
from plane.utils.integrations.github import get_github_repo_details
from plane.utils.importers.jira import jira_project_issue_summary
from plane.bgtasks.importer_task import service_importer
from plane.utils.html_processor import strip_tags
class ServiceIssueImportSummaryEndpoint(BaseAPIView):
def get(self, request, slug, service):
try:
if service == "github":
workspace_integration = WorkspaceIntegration.objects.get(
integration__provider="github", workspace__slug=slug
)
access_tokens_url = workspace_integration.metadata["access_tokens_url"]
owner = request.GET.get("owner")
repo = request.GET.get("repo")
issue_count, labels, collaborators = get_github_repo_details(
access_tokens_url, owner, repo
)
return Response(
{
"issue_count": issue_count,
"labels": labels,
"collaborators": collaborators,
},
status=status.HTTP_200_OK,
)
if service == "jira":
# Check for all the keys
params = {
"project_key": "Project key is required",
"api_token": "API token is required",
"email": "Email is required",
"cloud_hostname": "Cloud hostname is required",
}
for key, error_message in params.items():
if not request.GET.get(key, False):
return Response(
{"error": error_message}, status=status.HTTP_400_BAD_REQUEST
)
project_key = request.GET.get("project_key", "")
api_token = request.GET.get("api_token", "")
email = request.GET.get("email", "")
cloud_hostname = request.GET.get("cloud_hostname", "")
response = jira_project_issue_summary(
email, api_token, project_key, cloud_hostname
)
if "error" in response:
return Response(response, status=status.HTTP_400_BAD_REQUEST)
else:
return Response(
response,
status=status.HTTP_200_OK,
)
return Response(
{"error": "Service not supported yet"},
status=status.HTTP_400_BAD_REQUEST,
)
except WorkspaceIntegration.DoesNotExist:
return Response(
{"error": "Requested integration was not installed in the workspace"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class ImportServiceEndpoint(BaseAPIView):
def post(self, request, slug, service):
try:
project_id = request.data.get("project_id", False)
if not project_id:
return Response(
{"error": "Project ID is required"},
status=status.HTTP_400_BAD_REQUEST,
)
workspace = Workspace.objects.get(slug=slug)
if service == "github":
data = request.data.get("data", False)
metadata = request.data.get("metadata", False)
config = request.data.get("config", False)
if not data or not metadata or not config:
return Response(
{"error": "Data, config and metadata are required"},
status=status.HTTP_400_BAD_REQUEST,
)
api_token = APIToken.objects.filter(
user=request.user, workspace=workspace
).first()
if api_token is None:
api_token = APIToken.objects.create(
user=request.user,
label="Importer",
workspace=workspace,
)
importer = Importer.objects.create(
service=service,
project_id=project_id,
status="queued",
initiated_by=request.user,
data=data,
metadata=metadata,
token=api_token,
config=config,
created_by=request.user,
updated_by=request.user,
)
service_importer.delay(service, importer.id)
serializer = ImporterSerializer(importer)
return Response(serializer.data, status=status.HTTP_201_CREATED)
if service == "jira":
data = request.data.get("data", False)
metadata = request.data.get("metadata", False)
config = request.data.get("config", False)
if not data or not metadata:
return Response(
{"error": "Data, config and metadata are required"},
status=status.HTTP_400_BAD_REQUEST,
)
api_token = APIToken.objects.filter(
user=request.user, workspace=workspace
).first()
if api_token is None:
api_token = APIToken.objects.create(
user=request.user,
label="Importer",
workspace=workspace,
)
importer = Importer.objects.create(
service=service,
project_id=project_id,
status="queued",
initiated_by=request.user,
data=data,
metadata=metadata,
token=api_token,
config=config,
created_by=request.user,
updated_by=request.user,
)
service_importer.delay(service, importer.id)
serializer = ImporterSerializer(importer)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(
{"error": "Servivce not supported yet"},
status=status.HTTP_400_BAD_REQUEST,
)
except (
Workspace.DoesNotExist,
WorkspaceIntegration.DoesNotExist,
Project.DoesNotExist,
) as e:
return Response(
{"error": "Workspace Integration or Project does not exist"},
status=status.HTTP_404_NOT_FOUND,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def get(self, request, slug):
try:
imports = (
Importer.objects.filter(workspace__slug=slug)
.order_by("-created_at")
.select_related("initiated_by", "project", "workspace")
)
serializer = ImporterSerializer(imports, many=True)
return Response(serializer.data)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def delete(self, request, slug, service, pk):
try:
importer = Importer.objects.get(
pk=pk, service=service, workspace__slug=slug
)
# Delete all imported Issues
imported_issues = importer.imported_data.get("issues", [])
Issue.objects.filter(id__in=imported_issues).delete()
# Delete all imported Labels
imported_labels = importer.imported_data.get("labels", [])
Label.objects.filter(id__in=imported_labels).delete()
if importer.service == "jira":
imported_modules = importer.imported_data.get("modules", [])
Module.objects.filter(id__in=imported_modules).delete()
importer.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def patch(self, request, slug, service, pk):
try:
importer = Importer.objects.get(
pk=pk, service=service, workspace__slug=slug
)
serializer = ImporterSerializer(importer, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Importer.DoesNotExist:
return Response(
{"error": "Importer Does not exists"}, status=status.HTTP_404_NOT_FOUND
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class UpdateServiceImportStatusEndpoint(BaseAPIView):
def post(self, request, slug, project_id, service, importer_id):
try:
importer = Importer.objects.get(
pk=importer_id,
workspace__slug=slug,
project_id=project_id,
service=service,
)
importer.status = request.data.get("status", "processing")
importer.save()
return Response(status.HTTP_200_OK)
except Importer.DoesNotExist:
return Response(
{"error": "Importer does not exist"}, status=status.HTTP_404_NOT_FOUND
)
class BulkImportIssuesEndpoint(BaseAPIView):
def post(self, request, slug, project_id, service):
try:
# Get the project
project = Project.objects.get(pk=project_id, workspace__slug=slug)
# Get the default state
default_state = State.objects.filter(
project_id=project_id, default=True
).first()
# if there is no default state assign any random state
if default_state is None:
default_state = State.objects.filter(project_id=project_id).first()
# Get the maximum sequence_id
last_id = IssueSequence.objects.filter(project_id=project_id).aggregate(
largest=Max("sequence")
)["largest"]
last_id = 1 if last_id is None else last_id + 1
# Get the maximum sort order
largest_sort_order = Issue.objects.filter(
project_id=project_id, state=default_state
).aggregate(largest=Max("sort_order"))["largest"]
largest_sort_order = (
65535 if largest_sort_order is None else largest_sort_order + 10000
)
# Get the issues_data
issues_data = request.data.get("issues_data", [])
if not len(issues_data):
return Response(
{"error": "Issue data is required"},
status=status.HTTP_400_BAD_REQUEST,
)
# Issues
bulk_issues = []
for issue_data in issues_data:
bulk_issues.append(
Issue(
project_id=project_id,
workspace_id=project.workspace_id,
state_id=issue_data.get("state")
if issue_data.get("state", False)
else default_state.id,
name=issue_data.get("name", "Issue Created through Bulk"),
description_html=issue_data.get("description_html", "<p></p>"),
description_stripped=(
None
if (
issue_data.get("description_html") == ""
or issue_data.get("description_html") is None
)
else strip_tags(issue_data.get("description_html"))
),
sequence_id=last_id,
sort_order=largest_sort_order,
start_date=issue_data.get("start_date", None),
target_date=issue_data.get("target_date", None),
priority=issue_data.get("priority", None),
created_by=request.user,
)
)
largest_sort_order = largest_sort_order + 10000
last_id = last_id + 1
issues = Issue.objects.bulk_create(
bulk_issues,
batch_size=100,
ignore_conflicts=True,
)
# Sequences
_ = IssueSequence.objects.bulk_create(
[
IssueSequence(
issue=issue,
sequence=issue.sequence_id,
project_id=project_id,
workspace_id=project.workspace_id,
)
for issue in issues
],
batch_size=100,
)
# Attach Labels
bulk_issue_labels = []
for issue, issue_data in zip(issues, issues_data):
labels_list = issue_data.get("labels_list", [])
bulk_issue_labels = bulk_issue_labels + [
IssueLabel(
issue=issue,
label_id=label_id,
project_id=project_id,
workspace_id=project.workspace_id,
created_by=request.user,
)
for label_id in labels_list
]
_ = IssueLabel.objects.bulk_create(
bulk_issue_labels, batch_size=100, ignore_conflicts=True
)
# Attach Assignees
bulk_issue_assignees = []
for issue, issue_data in zip(issues, issues_data):
assignees_list = issue_data.get("assignees_list", [])
bulk_issue_assignees = bulk_issue_assignees + [
IssueAssignee(
issue=issue,
assignee_id=assignee_id,
project_id=project_id,
workspace_id=project.workspace_id,
created_by=request.user,
)
for assignee_id in assignees_list
]
_ = IssueAssignee.objects.bulk_create(
bulk_issue_assignees, batch_size=100, ignore_conflicts=True
)
# Track the issue activities
IssueActivity.objects.bulk_create(
[
IssueActivity(
issue=issue,
actor=request.user,
project_id=project_id,
workspace_id=project.workspace_id,
comment=f"{request.user.email} importer the issue from {service}",
verb="created",
created_by=request.user,
)
for issue in issues
],
batch_size=100,
)
# Create Comments
bulk_issue_comments = []
for issue, issue_data in zip(issues, issues_data):
comments_list = issue_data.get("comments_list", [])
bulk_issue_comments = bulk_issue_comments + [
IssueComment(
issue=issue,
comment_html=comment.get("comment_html", "<p></p>"),
actor=request.user,
project_id=project_id,
workspace_id=project.workspace_id,
created_by=request.user,
)
for comment in comments_list
]
_ = IssueComment.objects.bulk_create(bulk_issue_comments, batch_size=100)
# Attach Links
_ = IssueLink.objects.bulk_create(
[
IssueLink(
issue=issue,
url=issue_data.get("link", {}).get("url", "https://github.com"),
title=issue_data.get("link", {}).get("title", "Original Issue"),
project_id=project_id,
workspace_id=project.workspace_id,
created_by=request.user,
)
for issue, issue_data in zip(issues, issues_data)
]
)
return Response(
{"issues": IssueFlatSerializer(issues, many=True).data},
status=status.HTTP_201_CREATED,
)
except Project.DoesNotExist:
return Response(
{"error": "Project Does not exist"}, status=status.HTTP_404_NOT_FOUND
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class BulkImportModulesEndpoint(BaseAPIView):
def post(self, request, slug, project_id, service):
try:
modules_data = request.data.get("modules_data", [])
project = Project.objects.get(pk=project_id, workspace__slug=slug)
modules = Module.objects.bulk_create(
[
Module(
name=module.get("name", uuid.uuid4().hex),
description=module.get("description", ""),
start_date=module.get("start_date", None),
target_date=module.get("target_date", None),
project_id=project_id,
workspace_id=project.workspace_id,
created_by=request.user,
)
for module in modules_data
],
batch_size=100,
ignore_conflicts=True,
)
modules = Module.objects.filter(id__in=[module.id for module in modules])
if len(modules) == len(modules_data):
_ = ModuleLink.objects.bulk_create(
[
ModuleLink(
module=module,
url=module_data.get("link", {}).get(
"url", "https://plane.so"
),
title=module_data.get("link", {}).get(
"title", "Original Issue"
),
project_id=project_id,
workspace_id=project.workspace_id,
created_by=request.user,
)
for module, module_data in zip(modules, modules_data)
],
batch_size=100,
ignore_conflicts=True,
)
bulk_module_issues = []
for module, module_data in zip(modules, modules_data):
module_issues_list = module_data.get("module_issues_list", [])
bulk_module_issues = bulk_module_issues + [
ModuleIssue(
issue_id=issue,
module=module,
project_id=project_id,
workspace_id=project.workspace_id,
created_by=request.user,
)
for issue in module_issues_list
]
_ = ModuleIssue.objects.bulk_create(
bulk_module_issues, batch_size=100, ignore_conflicts=True
)
serializer = ModuleSerializer(modules, many=True)
return Response(
{"modules": serializer.data}, status=status.HTTP_201_CREATED
)
else:
return Response(
{"message": "Modules created but issues could not be imported"},
status=status.HTTP_200_OK,
)
except Project.DoesNotExist:
return Response(
{"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -2,6 +2,8 @@ from .base import IntegrationViewSet, WorkspaceIntegrationViewSet
from .github import (
GithubRepositorySyncViewSet,
GithubIssueSyncViewSet,
BulkCreateGithubIssueSyncEndpoint,
GithubCommentSyncViewSet,
GithubRepositoriesEndpoint,
)
from .slack import SlackProjectSyncViewSet
+39 -8
View File
@@ -25,6 +25,7 @@ from plane.utils.integrations.github import (
get_github_metadata,
delete_github_installation,
)
from plane.api.permissions import WorkSpaceAdminPermission
class IntegrationViewSet(BaseViewSet):
@@ -75,11 +76,32 @@ class IntegrationViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
def destroy(self, request, pk):
try:
integration = Integration.objects.get(pk=pk)
if integration.verified:
return Response(
{"error": "Verified integrations cannot be updated"},
status=status.HTTP_400_BAD_REQUEST,
)
integration.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except Integration.DoesNotExist:
return Response(
{"error": "Integration Does not exist"},
status=status.HTTP_404_NOT_FOUND,
)
class WorkspaceIntegrationViewSet(BaseViewSet):
serializer_class = WorkspaceIntegrationSerializer
model = WorkspaceIntegration
permission_classes = [
WorkSpaceAdminPermission,
]
def get_queryset(self):
return (
super()
@@ -90,21 +112,30 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
def create(self, request, slug, provider):
try:
installation_id = request.data.get("installation_id", None)
if not installation_id:
return Response(
{"error": "Installation ID is required"},
status=status.HTTP_400_BAD_REQUEST,
)
workspace = Workspace.objects.get(slug=slug)
integration = Integration.objects.get(provider=provider)
config = {}
if provider == "github":
installation_id = request.data.get("installation_id", None)
if not installation_id:
return Response(
{"error": "Installation ID is required"},
status=status.HTTP_400_BAD_REQUEST,
)
metadata = get_github_metadata(installation_id)
config = {"installation_id": installation_id}
if provider == "slack":
metadata = request.data.get("metadata", {})
access_token = metadata.get("access_token", False)
team_id = metadata.get("team", {}).get("id", False)
if not metadata or not access_token or not team_id:
return Response(
{"error": "Access token and team id is required"},
status=status.HTTP_400_BAD_REQUEST,
)
config = {"team_id": team_id, "access_token": access_token}
# Create a bot user
bot_user = User.objects.create(
email=f"{uuid.uuid4().hex}@plane.so",
@@ -13,6 +13,7 @@ from plane.db.models import (
ProjectMember,
Label,
GithubCommentSync,
Project,
)
from plane.api.serializers import (
GithubIssueSyncSerializer,
@@ -20,15 +21,27 @@ from plane.api.serializers import (
GithubCommentSyncSerializer,
)
from plane.utils.integrations.github import get_github_repos
from plane.api.permissions import ProjectBasePermission, ProjectEntityPermission
class GithubRepositoriesEndpoint(BaseAPIView):
permission_classes = [
ProjectBasePermission,
]
def get(self, request, slug, workspace_integration_id):
try:
page = request.GET.get("page", 1)
workspace_integration = WorkspaceIntegration.objects.get(
workspace__slug=slug, pk=workspace_integration_id
)
if workspace_integration.integration.provider != "github":
return Response(
{"error": "Not a github integration"},
status=status.HTTP_400_BAD_REQUEST,
)
access_tokens_url = workspace_integration.metadata["access_tokens_url"]
repositories_url = (
workspace_integration.metadata["repositories_url"]
@@ -44,6 +57,10 @@ class GithubRepositoriesEndpoint(BaseAPIView):
class GithubRepositorySyncViewSet(BaseViewSet):
permission_classes = [
ProjectBasePermission,
]
serializer_class = GithubRepositorySyncSerializer
model = GithubRepositorySync
@@ -84,10 +101,6 @@ class GithubRepositorySyncViewSet(BaseViewSet):
GithubRepository.objects.filter(
project_id=project_id, workspace__slug=slug
).delete()
# Project member delete
ProjectMember.objects.filter(
member=workspace_integration.actor, role=20, project_id=project_id
).delete()
# Create repository
repo = GithubRepository.objects.create(
@@ -124,7 +137,7 @@ class GithubRepositorySyncViewSet(BaseViewSet):
)
# Add bot as a member in the project
_ = ProjectMember.objects.create(
_ = ProjectMember.objects.get_or_create(
member=workspace_integration.actor, role=20, project_id=project_id
)
@@ -148,6 +161,10 @@ class GithubRepositorySyncViewSet(BaseViewSet):
class GithubIssueSyncViewSet(BaseViewSet):
permission_classes = [
ProjectEntityPermission,
]
serializer_class = GithubIssueSyncSerializer
model = GithubIssueSync
@@ -158,7 +175,52 @@ class GithubIssueSyncViewSet(BaseViewSet):
)
class BulkCreateGithubIssueSyncEndpoint(BaseAPIView):
def post(self, request, slug, project_id, repo_sync_id):
try:
project = Project.objects.get(pk=project_id, workspace__slug=slug)
github_issue_syncs = request.data.get("github_issue_syncs", [])
github_issue_syncs = GithubIssueSync.objects.bulk_create(
[
GithubIssueSync(
issue_id=github_issue_sync.get("issue"),
repo_issue_id=github_issue_sync.get("repo_issue_id"),
issue_url=github_issue_sync.get("issue_url"),
github_issue_id=github_issue_sync.get("github_issue_id"),
repository_sync_id=repo_sync_id,
project_id=project_id,
workspace_id=project.workspace_id,
created_by=request.user,
updated_by=request.user,
)
for github_issue_sync in github_issue_syncs
],
batch_size=100,
ignore_conflicts=True,
)
serializer = GithubIssueSyncSerializer(github_issue_syncs, many=True)
return Response(serializer.data, status=status.HTTP_201_CREATED)
except Project.DoesNotExist:
return Response(
{"error": "Project does not exist"},
status=status.HTTP_404_NOT_FOUND,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class GithubCommentSyncViewSet(BaseViewSet):
permission_classes = [
ProjectEntityPermission,
]
serializer_class = GithubCommentSyncSerializer
model = GithubCommentSync
@@ -0,0 +1,59 @@
# 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 create(self, request, slug, project_id, workspace_integration_id):
try:
serializer = SlackProjectSyncSerializer(data=request.data)
workspace_integration = WorkspaceIntegration.objects.get(
workspace__slug=slug, pk=workspace_integration_id
)
if serializer.is_valid():
serializer.save(
project_id=project_id,
workspace_integration_id=workspace_integration_id,
)
workspace_integration = WorkspaceIntegration.objects.get(
pk=workspace_integration_id, workspace__slug=slug
)
_ = ProjectMember.objects.get_or_create(
member=workspace_integration.actor, role=20, project_id=project_id
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError:
return Response({"error": "Slack is already enabled for the project"}, status=status.HTTP_400_BAD_REQUEST)
except WorkspaceIntegration.DoesNotExist:
return Response(
{"error": "Workspace Integration does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
print(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
+307 -182
View File
@@ -1,14 +1,19 @@
# Python imports
import json
from itertools import groupby, chain
import random
from itertools import chain
# Django imports
from django.db.models import Prefetch, OuterRef, Func, F, Q
from django.db.models import Prefetch, OuterRef, Func, F, Q, Count
from django.core.serializers.json import DjangoJSONEncoder
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
from django.db.models.functions import Coalesce
# Third Party imports
from rest_framework.response import Response
from rest_framework import status
from rest_framework.parsers import MultiPartParser, FormParser
from sentry_sdk import capture_exception
# Module imports
@@ -24,6 +29,8 @@ from plane.api.serializers import (
LabelSerializer,
IssueFlatSerializer,
IssueLinkSerializer,
IssueLiteSerializer,
IssueAttachmentSerializer,
)
from plane.api.permissions import (
ProjectEntityPermission,
@@ -38,13 +45,13 @@ from plane.db.models import (
TimelineIssue,
IssueProperty,
Label,
IssueBlocker,
CycleIssue,
ModuleIssue,
IssueLink,
IssueAttachment,
State,
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.grouper import group_results
from plane.utils.issue_filters import issue_filters
class IssueViewSet(BaseViewSet):
@@ -80,16 +87,14 @@ class IssueViewSet(BaseViewSet):
)
if current_instance is not None:
issue_activity.delay(
{
"type": "issue.activity.updated",
"requested_data": requested_data,
"actor_id": str(self.request.user.id),
"issue_id": str(self.kwargs.get("pk", None)),
"project_id": str(self.kwargs.get("project_id", None)),
"current_instance": json.dumps(
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
),
},
type="issue.activity.updated",
requested_data=requested_data,
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
),
)
return super().perform_update(serializer)
@@ -100,18 +105,16 @@ class IssueViewSet(BaseViewSet):
)
if current_instance is not None:
issue_activity.delay(
{
"type": "issue.activity.deleted",
"requested_data": json.dumps(
{"issue_id": str(self.kwargs.get("pk", None))}
),
"actor_id": str(self.request.user.id),
"issue_id": str(self.kwargs.get("pk", None)),
"project_id": str(self.kwargs.get("project_id", None)),
"current_instance": json.dumps(
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
),
},
type="issue.activity.deleted",
requested_data=json.dumps(
{"issue_id": str(self.kwargs.get("pk", None))}
),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
),
)
return super().perform_destroy(instance)
@@ -133,59 +136,43 @@ class IssueViewSet(BaseViewSet):
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
.prefetch_related(
Prefetch(
"blocked_issues",
queryset=IssueBlocker.objects.select_related("blocked_by", "block"),
)
)
.prefetch_related(
Prefetch(
"blocker_issues",
queryset=IssueBlocker.objects.select_related("block", "blocked_by"),
)
)
.prefetch_related(
Prefetch(
"issue_cycle",
queryset=CycleIssue.objects.select_related("cycle", "issue"),
),
)
.prefetch_related(
Prefetch(
"issue_module",
queryset=ModuleIssue.objects.select_related(
"module", "issue"
).prefetch_related("module__members"),
),
)
.prefetch_related(
Prefetch(
"issue_link",
queryset=IssueLink.objects.select_related("issue").select_related(
"created_by"
),
)
)
)
@method_decorator(gzip_page)
def list(self, request, slug, project_id):
try:
# Issue State groups
type = request.GET.get("type", "all")
group = ["backlog", "unstarted", "started", "completed", "cancelled"]
if type == "backlog":
group = ["backlog"]
if type == "active":
group = ["unstarted", "started"]
filters = issue_filters(request.query_params, "GET")
show_sub_issues = request.GET.get("show_sub_issues", "true")
issue_queryset = (
self.get_queryset()
.order_by(request.GET.get("order_by", "created_at"))
.filter(state__group__in=group)
.filter(**filters)
.annotate(cycle_id=F("issue_cycle__id"))
.annotate(module_id=F("issue_module__id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=IssueAttachment.objects.filter(
issue=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
issues = IssueSerializer(issue_queryset, many=True).data
issue_queryset = (
issue_queryset
if show_sub_issues == "true"
else issue_queryset.filter(parent__isnull=True)
)
issues = IssueLiteSerializer(issue_queryset, many=True).data
## Grouping the results
group_by = request.GET.get("group_by", False)
@@ -197,7 +184,6 @@ class IssueViewSet(BaseViewSet):
return Response(issues, status=status.HTTP_200_OK)
except Exception as e:
print(e)
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
@@ -216,16 +202,12 @@ class IssueViewSet(BaseViewSet):
# Track the issue
issue_activity.delay(
{
"type": "issue.activity.created",
"requested_data": json.dumps(
self.request.data, cls=DjangoJSONEncoder
),
"actor_id": str(request.user.id),
"issue_id": str(serializer.data.get("id", None)),
"project_id": str(project_id),
"current_instance": None,
},
type="issue.activity.created",
requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
actor_id=str(request.user.id),
issue_id=str(serializer.data.get("id", None)),
project_id=str(project_id),
current_instance=None,
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -235,8 +217,20 @@ class IssueViewSet(BaseViewSet):
{"error": "Project was not found"}, status=status.HTTP_404_NOT_FOUND
)
def retrieve(self, request, slug, project_id, pk=None):
try:
issue = Issue.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
except Issue.DoesNotExist:
return Response(
{"error": "Issue Does not exist"}, status=status.HTTP_404_NOT_FOUND
)
class UserWorkSpaceIssues(BaseAPIView):
@method_decorator(gzip_page)
def get(self, request, slug):
try:
issues = (
@@ -253,44 +247,23 @@ class UserWorkSpaceIssues(BaseAPIView):
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
.prefetch_related(
Prefetch(
"blocked_issues",
queryset=IssueBlocker.objects.select_related(
"blocked_by", "block"
),
)
)
.prefetch_related(
Prefetch(
"blocker_issues",
queryset=IssueBlocker.objects.select_related(
"block", "blocked_by"
),
)
)
.prefetch_related(
Prefetch(
"issue_cycle",
queryset=CycleIssue.objects.select_related("cycle", "issue"),
),
)
.prefetch_related(
Prefetch(
"issue_module",
queryset=ModuleIssue.objects.select_related("module", "issue"),
),
)
.prefetch_related(
Prefetch(
"issue_link",
queryset=IssueLink.objects.select_related(
"issue"
).select_related("created_by"),
.order_by("-created_at")
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=IssueAttachment.objects.filter(
issue=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
serializer = IssueSerializer(issues, many=True)
serializer = IssueLiteSerializer(issues, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
@@ -305,10 +278,13 @@ class WorkSpaceIssuesEndpoint(BaseAPIView):
WorkSpaceAdminPermission,
]
@method_decorator(gzip_page)
def get(self, request, slug):
try:
issues = Issue.objects.filter(workspace__slug=slug).filter(
project__project_projectmember__member=self.request.user
issues = (
Issue.objects.filter(workspace__slug=slug)
.filter(project__project_projectmember__member=self.request.user)
.order_by("-created_at")
)
serializer = IssueSerializer(issues, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
@@ -325,6 +301,7 @@ class IssueActivityEndpoint(BaseAPIView):
ProjectEntityPermission,
]
@method_decorator(gzip_page)
def get(self, request, slug, project_id, issue_id):
try:
issue_activities = (
@@ -333,8 +310,8 @@ class IssueActivityEndpoint(BaseAPIView):
~Q(field="comment"),
project__project_projectmember__member=self.request.user,
)
.select_related("actor")
).order_by("created_by")
.select_related("actor", "workspace")
).order_by("created_at")
issue_comments = (
IssueComment.objects.filter(issue_id=issue_id)
.filter(project__project_projectmember__member=self.request.user)
@@ -376,14 +353,12 @@ class IssueCommentViewSet(BaseViewSet):
actor=self.request.user if self.request.user is not None else None,
)
issue_activity.delay(
{
"type": "comment.activity.created",
"requested_data": json.dumps(serializer.data, cls=DjangoJSONEncoder),
"actor_id": str(self.request.user.id),
"issue_id": str(self.kwargs.get("issue_id")),
"project_id": str(self.kwargs.get("project_id")),
"current_instance": None,
},
type="comment.activity.created",
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id")),
project_id=str(self.kwargs.get("project_id")),
current_instance=None,
)
def perform_update(self, serializer):
@@ -393,17 +368,15 @@ class IssueCommentViewSet(BaseViewSet):
)
if current_instance is not None:
issue_activity.delay(
{
"type": "comment.activity.updated",
"requested_data": requested_data,
"actor_id": str(self.request.user.id),
"issue_id": str(self.kwargs.get("issue_id", None)),
"project_id": str(self.kwargs.get("project_id", None)),
"current_instance": json.dumps(
IssueCommentSerializer(current_instance).data,
cls=DjangoJSONEncoder,
),
},
type="comment.activity.updated",
requested_data=requested_data,
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
IssueCommentSerializer(current_instance).data,
cls=DjangoJSONEncoder,
),
)
return super().perform_update(serializer)
@@ -414,19 +387,17 @@ class IssueCommentViewSet(BaseViewSet):
)
if current_instance is not None:
issue_activity.delay(
{
"type": "comment.activity.deleted",
"requested_data": json.dumps(
{"comment_id": str(self.kwargs.get("pk", None))}
),
"actor_id": str(self.request.user.id),
"issue_id": str(self.kwargs.get("issue_id", None)),
"project_id": str(self.kwargs.get("project_id", None)),
"current_instance": json.dumps(
IssueCommentSerializer(current_instance).data,
cls=DjangoJSONEncoder,
),
},
type="comment.activity.deleted",
requested_data=json.dumps(
{"comment_id": str(self.kwargs.get("pk", None))}
),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
IssueCommentSerializer(current_instance).data,
cls=DjangoJSONEncoder,
),
)
return super().perform_destroy(instance)
@@ -561,6 +532,7 @@ class LabelViewSet(BaseViewSet):
.select_related("project")
.select_related("workspace")
.select_related("parent")
.order_by("name")
.distinct()
)
@@ -605,6 +577,7 @@ class SubIssuesEndpoint(BaseAPIView):
ProjectEntityPermission,
]
@method_decorator(gzip_page)
def get(self, request, slug, project_id, issue_id):
try:
sub_issues = (
@@ -617,38 +590,33 @@ class SubIssuesEndpoint(BaseAPIView):
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
.prefetch_related(
Prefetch(
"blocked_issues",
queryset=IssueBlocker.objects.select_related(
"blocked_by", "block"
),
)
)
.prefetch_related(
Prefetch(
"blocker_issues",
queryset=IssueBlocker.objects.select_related(
"block", "blocked_by"
),
)
)
.prefetch_related(
Prefetch(
"issue_cycle",
queryset=CycleIssue.objects.select_related("cycle", "issue"),
),
)
.prefetch_related(
Prefetch(
"issue_module",
queryset=ModuleIssue.objects.select_related("module", "issue"),
),
)
)
serializer = IssueSerializer(sub_issues, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
state_distribution = (
State.objects.filter(workspace__slug=slug, project_id=project_id)
.annotate(
state_count=Count(
"state_issue",
filter=Q(state_issue__parent_id=issue_id),
)
)
.order_by("group")
.values("group", "state_count")
)
result = {item["group"]: item["state_count"] for item in state_distribution}
serializer = IssueLiteSerializer(
sub_issues,
many=True,
)
return Response(
{
"sub_issues": serializer.data,
"state_distribution": result,
},
status=status.HTTP_200_OK,
)
except Exception as e:
capture_exception(e)
return Response(
@@ -706,6 +674,54 @@ class IssueLinkViewSet(BaseViewSet):
project_id=self.kwargs.get("project_id"),
issue_id=self.kwargs.get("issue_id"),
)
issue_activity.delay(
type="link.activity.created",
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id")),
project_id=str(self.kwargs.get("project_id")),
current_instance=None,
)
def perform_update(self, serializer):
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
current_instance = (
self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
)
if current_instance is not None:
issue_activity.delay(
type="link.activity.updated",
requested_data=requested_data,
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
IssueLinkSerializer(current_instance).data,
cls=DjangoJSONEncoder,
),
)
return super().perform_update(serializer)
def perform_destroy(self, instance):
current_instance = (
self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
)
if current_instance is not None:
issue_activity.delay(
type="link.activity.deleted",
requested_data=json.dumps(
{"link_id": str(self.kwargs.get("pk", None))}
),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
IssueLinkSerializer(current_instance).data,
cls=DjangoJSONEncoder,
),
)
return super().perform_destroy(instance)
def get_queryset(self):
return (
@@ -715,5 +731,114 @@ class IssueLinkViewSet(BaseViewSet):
.filter(project_id=self.kwargs.get("project_id"))
.filter(issue_id=self.kwargs.get("issue_id"))
.filter(project__project_projectmember__member=self.request.user)
.order_by("-created_at")
.distinct()
)
class BulkCreateIssueLabelsEndpoint(BaseAPIView):
def post(self, request, slug, project_id):
try:
label_data = request.data.get("label_data", [])
project = Project.objects.get(pk=project_id)
labels = Label.objects.bulk_create(
[
Label(
name=label.get("name", "Migrated"),
description=label.get("description", "Migrated Issue"),
color="#" + "%06x" % random.randint(0, 0xFFFFFF),
project_id=project_id,
workspace_id=project.workspace_id,
created_by=request.user,
updated_by=request.user,
)
for label in label_data
],
batch_size=50,
ignore_conflicts=True,
)
return Response(
{"labels": LabelSerializer(labels, many=True).data},
status=status.HTTP_201_CREATED,
)
except Project.DoesNotExist:
return Response(
{"error": "Project Does not exist"}, status=status.HTTP_404_NOT_FOUND
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class IssueAttachmentEndpoint(BaseAPIView):
serializer_class = IssueAttachmentSerializer
permission_classes = [
ProjectEntityPermission,
]
model = IssueAttachment
parser_classes = (MultiPartParser, FormParser)
def post(self, request, slug, project_id, issue_id):
try:
serializer = IssueAttachmentSerializer(data=request.data)
if serializer.is_valid():
serializer.save(project_id=project_id, issue_id=issue_id)
issue_activity.delay(
type="attachment.activity.created",
requested_data=None,
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
serializer.data,
cls=DjangoJSONEncoder,
),
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def delete(self, request, slug, project_id, issue_id, pk):
try:
issue_attachment = IssueAttachment.objects.get(pk=pk)
issue_attachment.asset.delete(save=False)
issue_attachment.delete()
issue_activity.delay(
type="attachment.activity.deleted",
requested_data=None,
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
)
return Response(status=status.HTTP_204_NO_CONTENT)
except IssueAttachment.DoesNotExist:
return Response(
{"error": "Issue Attachment does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
def get(self, request, slug, project_id, issue_id):
try:
issue_attachments = IssueAttachment.objects.filter(
issue_id=issue_id, workspace__slug=slug, project_id=project_id
)
serilaizer = IssueAttachmentSerializer(issue_attachments, many=True)
return Response(serilaizer.data, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
+142 -46
View File
@@ -3,8 +3,10 @@ import json
# Django Imports
from django.db import IntegrityError
from django.db.models import Prefetch, F, OuterRef, Func, Exists
from django.db.models import Prefetch, F, OuterRef, Func, Exists, Count, Q
from django.core import serializers
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
# Third party imports
from rest_framework.response import Response
@@ -19,6 +21,7 @@ from plane.api.serializers import (
ModuleIssueSerializer,
ModuleLinkSerializer,
ModuleFavoriteSerializer,
IssueStateSerializer,
)
from plane.api.permissions import ProjectEntityPermission
from plane.db.models import (
@@ -28,9 +31,12 @@ from plane.db.models import (
Issue,
ModuleLink,
ModuleFavorite,
IssueLink,
IssueAttachment,
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.grouper import group_results
from plane.utils.issue_filters import issue_filters
class ModuleViewSet(BaseViewSet):
@@ -47,31 +53,84 @@ class ModuleViewSet(BaseViewSet):
)
def get_queryset(self):
subquery = ModuleFavorite.objects.filter(
user=self.request.user,
module_id=OuterRef("pk"),
project_id=self.kwargs.get("project_id"),
workspace__slug=self.kwargs.get("slug"),
)
return (
super()
.get_queryset()
.filter(project_id=self.kwargs.get("project_id"))
.filter(workspace__slug=self.kwargs.get("slug"))
.annotate(is_favorite=Exists(subquery))
.select_related("project")
.select_related("workspace")
.select_related("lead")
.prefetch_related("members")
.prefetch_related(
Prefetch(
"issue_module",
queryset=ModuleIssue.objects.select_related(
"module", "issue", "issue__state", "issue__project"
).prefetch_related("issue__assignees", "issue__labels"),
)
)
.prefetch_related(
Prefetch(
"link_module",
queryset=ModuleLink.objects.select_related("module", "created_by"),
)
)
.annotate(total_issues=Count("issue_module"))
.annotate(
completed_issues=Count(
"issue_module__issue__state__group",
filter=Q(issue_module__issue__state__group="completed"),
)
)
.annotate(
cancelled_issues=Count(
"issue_module__issue__state__group",
filter=Q(issue_module__issue__state__group="cancelled"),
)
)
.annotate(
started_issues=Count(
"issue_module__issue__state__group",
filter=Q(issue_module__issue__state__group="started"),
)
)
.annotate(
unstarted_issues=Count(
"issue_module__issue__state__group",
filter=Q(issue_module__issue__state__group="unstarted"),
)
)
.annotate(
backlog_issues=Count(
"issue_module__issue__state__group",
filter=Q(issue_module__issue__state__group="backlog"),
)
)
.order_by("-is_favorite", "name")
)
def perform_destroy(self, instance):
module_issues = list(
ModuleIssue.objects.filter(module_id=self.kwargs.get("pk")).values_list(
"issue", flat=True
)
)
issue_activity.delay(
type="module.activity.deleted",
requested_data=json.dumps(
{
"module_id": str(self.kwargs.get("pk")),
"issues": [str(issue_id) for issue_id in module_issues],
}
),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
)
return super().perform_destroy(instance)
def create(self, request, slug, project_id):
try:
project = Project.objects.get(workspace__slug=slug, pk=project_id)
@@ -101,23 +160,6 @@ class ModuleViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
def list(self, request, slug, project_id):
try:
subquery = ModuleFavorite.objects.filter(
user=self.request.user,
module_id=OuterRef("pk"),
project_id=project_id,
workspace__slug=slug,
)
modules = self.get_queryset().annotate(is_favorite=Exists(subquery))
return Response(ModuleSerializer(modules, many=True).data)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class ModuleIssueViewSet(BaseViewSet):
serializer_class = ModuleIssueSerializer
@@ -138,6 +180,22 @@ class ModuleIssueViewSet(BaseViewSet):
module_id=self.kwargs.get("module_id"),
)
def perform_destroy(self, instance):
issue_activity.delay(
type="module.activity.deleted",
requested_data=json.dumps(
{
"module_id": str(self.kwargs.get("module_id")),
"issues": [str(instance.issue_id)],
}
),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
)
return super().perform_destroy(instance)
def get_queryset(self):
return self.filter_queryset(
super()
@@ -161,22 +219,57 @@ class ModuleIssueViewSet(BaseViewSet):
.distinct()
)
@method_decorator(gzip_page)
def list(self, request, slug, project_id, module_id):
try:
order_by = request.GET.get("order_by", "issue__created_at")
queryset = self.get_queryset().order_by(f"issue__{order_by}")
order_by = request.GET.get("order_by", "created_at")
group_by = request.GET.get("group_by", False)
filters = issue_filters(request.query_params, "GET")
issues = (
Issue.objects.filter(issue_module__module_id=module_id)
.annotate(
sub_issues_count=Issue.objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(bridge_id=F("issue_module__id"))
.filter(project_id=project_id)
.filter(workspace__slug=slug)
.select_related("project")
.select_related("workspace")
.select_related("state")
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
.order_by(order_by)
.filter(**filters)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=IssueAttachment.objects.filter(
issue=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
module_issues = ModuleIssueSerializer(queryset, many=True).data
issues_data = IssueStateSerializer(issues, many=True).data
if group_by:
return Response(
group_results(module_issues, f"issue_detail.{group_by}"),
group_results(issues_data, group_by),
status=status.HTTP_200_OK,
)
return Response(
module_issues,
issues_data,
status=status.HTTP_200_OK,
)
except Exception as e:
@@ -247,21 +340,19 @@ class ModuleIssueViewSet(BaseViewSet):
# Capture Issue Activity
issue_activity.delay(
{
"type": "issue.activity",
"requested_data": json.dumps({"modules_list": issues}),
"actor_id": str(self.request.user.id),
"issue_id": str(self.kwargs.get("pk", None)),
"project_id": str(self.kwargs.get("project_id", None)),
"current_instance": json.dumps(
{
"updated_module_issues": update_module_issue_activity,
"created_module_issues": serializers.serialize(
"json", record_to_create
),
}
),
},
type="module.activity.created",
requested_data=json.dumps({"modules_list": issues}),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
{
"updated_module_issues": update_module_issue_activity,
"created_module_issues": serializers.serialize(
"json", record_to_create
),
}
),
)
return Response(
@@ -302,11 +393,16 @@ class ModuleLinkViewSet(BaseViewSet):
.filter(project_id=self.kwargs.get("project_id"))
.filter(module_id=self.kwargs.get("module_id"))
.filter(project__project_projectmember__member=self.request.user)
.order_by("-created_at")
.distinct()
)
class ModuleFavoriteViewSet(BaseViewSet):
permission_classes = [
ProjectEntityPermission,
]
serializer_class = ModuleFavoriteSerializer
model = ModuleFavorite
+44 -5
View File
@@ -5,6 +5,7 @@ import os
# Django imports
from django.utils import timezone
from django.conf import settings
# Third Party modules
from rest_framework.response import Response
@@ -13,7 +14,7 @@ from rest_framework.permissions import AllowAny
from rest_framework.views import APIView
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework import status
from sentry_sdk import capture_exception
# sso authentication
from google.oauth2 import id_token
from google.auth.transport import requests as google_auth_request
@@ -47,7 +48,7 @@ def validate_google_token(token, client_id):
}
return data
except Exception as e:
print(e)
capture_exception(e)
raise exceptions.AuthenticationFailed("Error with Google connection.")
@@ -204,7 +205,26 @@ class OauthEndpoint(BaseAPIView):
"last_login_at": timezone.now(),
},
)
if settings.ANALYTICS_BASE_API:
_ = requests.post(
settings.ANALYTICS_BASE_API,
headers={
"Content-Type": "application/json",
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
},
json={
"event_id": uuid.uuid4().hex,
"event_data": {
"medium": f"oauth-{medium}",
},
"user": {"email": email, "id": str(user.id)},
"device_ctx": {
"ip": request.META.get("REMOTE_ADDR"),
"user_agent": request.META.get("HTTP_USER_AGENT"),
},
"event_type": "SIGN_IN",
},
)
return Response(data, status=status.HTTP_200_OK)
except User.DoesNotExist:
@@ -253,6 +273,26 @@ class OauthEndpoint(BaseAPIView):
"user": serialized_user,
"permissions": [],
}
if settings.ANALYTICS_BASE_API:
_ = requests.post(
settings.ANALYTICS_BASE_API,
headers={
"Content-Type": "application/json",
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
},
json={
"event_id": uuid.uuid4().hex,
"event_data": {
"medium": f"oauth-{medium}",
},
"user": {"email": email, "id": str(user.id)},
"device_ctx": {
"ip": request.META.get("REMOTE_ADDR"),
"user_agent": request.META.get("HTTP_USER_AGENT"),
},
"event_type": "SIGN_UP",
},
)
SocialLoginConnection.objects.update_or_create(
medium=medium,
@@ -265,8 +305,7 @@ class OauthEndpoint(BaseAPIView):
)
return Response(data, status=status.HTTP_201_CREATED)
except Exception as e:
print(e)
capture_exception(e)
return Response(
{
"error": "Something went wrong. Please try again later or contact the support team."
+517
View File
@@ -0,0 +1,517 @@
# Python imports
from datetime import timedelta, datetime, date
# Django imports
from django.db import IntegrityError
from django.db.models import Exists, OuterRef, Q, Prefetch
from django.utils import timezone
# Third party imports
from rest_framework import status
from rest_framework.response import Response
from sentry_sdk import capture_exception
# Module imports
from .base import BaseViewSet, BaseAPIView
from plane.api.permissions import ProjectEntityPermission
from plane.db.models import (
Page,
PageBlock,
PageFavorite,
Issue,
IssueAssignee,
IssueActivity,
)
from plane.api.serializers import (
PageSerializer,
PageBlockSerializer,
PageFavoriteSerializer,
IssueLiteSerializer,
)
class PageViewSet(BaseViewSet):
serializer_class = PageSerializer
model = Page
permission_classes = [
ProjectEntityPermission,
]
search_fields = [
"name",
]
def get_queryset(self):
subquery = PageFavorite.objects.filter(
user=self.request.user,
page_id=OuterRef("pk"),
project_id=self.kwargs.get("project_id"),
workspace__slug=self.kwargs.get("slug"),
)
return self.filter_queryset(
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(project__project_projectmember__member=self.request.user)
.filter(Q(owned_by=self.request.user) | Q(access=0))
.select_related("project")
.select_related("workspace")
.select_related("owned_by")
.annotate(is_favorite=Exists(subquery))
.order_by(self.request.GET.get("order_by", "-created_at"))
.prefetch_related("labels")
.order_by("name", "-is_favorite")
.prefetch_related(
Prefetch(
"blocks",
queryset=PageBlock.objects.select_related(
"page", "issue", "workspace", "project"
),
)
)
.distinct()
)
def perform_create(self, serializer):
serializer.save(
project_id=self.kwargs.get("project_id"), owned_by=self.request.user
)
def create(self, request, slug, project_id):
try:
serializer = PageSerializer(
data=request.data,
context={"project_id": project_id, "owned_by_id": request.user.id},
)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def partial_update(self, request, slug, project_id, pk):
try:
page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
# Only update access if the page owner is the requesting user
if (
page.access != request.data.get("access", page.access)
and page.owned_by_id != request.user.id
):
return Response(
{
"error": "Access cannot be updated since this page is owned by someone else"
},
status=status.HTTP_400_BAD_REQUEST,
)
serializer = PageSerializer(page, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Page.DoesNotExist:
return Response(
{"error": "Page Does not exist"}, status=status.HTTP_400_BAD_REQUEST
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class PageBlockViewSet(BaseViewSet):
serializer_class = PageBlockSerializer
model = PageBlock
permission_classes = [
ProjectEntityPermission,
]
def get_queryset(self):
return self.filter_queryset(
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(page_id=self.kwargs.get("page_id"))
.filter(project__project_projectmember__member=self.request.user)
.select_related("project")
.select_related("workspace")
.select_related("page")
.select_related("issue")
.order_by("sort_order")
.distinct()
)
def perform_create(self, serializer):
serializer.save(
project_id=self.kwargs.get("project_id"),
page_id=self.kwargs.get("page_id"),
)
class PageFavoriteViewSet(BaseViewSet):
permission_classes = [
ProjectEntityPermission,
]
serializer_class = PageFavoriteSerializer
model = PageFavorite
def get_queryset(self):
return self.filter_queryset(
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(user=self.request.user)
.select_related("page", "page__owned_by")
)
def create(self, request, slug, project_id):
try:
serializer = PageFavoriteSerializer(data=request.data)
if serializer.is_valid():
serializer.save(user=request.user, project_id=project_id)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError as e:
if "already exists" in str(e):
return Response(
{"error": "The page is already added to favorites"},
status=status.HTTP_410_GONE,
)
else:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def destroy(self, request, slug, project_id, page_id):
try:
page_favorite = PageFavorite.objects.get(
project=project_id,
user=request.user,
workspace__slug=slug,
page_id=page_id,
)
page_favorite.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except PageFavorite.DoesNotExist:
return Response(
{"error": "Page is not in favorites"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class CreateIssueFromPageBlockEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def post(self, request, slug, project_id, page_id, page_block_id):
try:
page_block = PageBlock.objects.get(
pk=page_block_id,
workspace__slug=slug,
project_id=project_id,
page_id=page_id,
)
issue = Issue.objects.create(
name=page_block.name,
project_id=project_id,
description=page_block.description,
description_html=page_block.description_html,
description_stripped=page_block.description_stripped,
)
_ = IssueAssignee.objects.create(
issue=issue, assignee=request.user, project_id=project_id
)
_ = IssueActivity.objects.create(
issue=issue,
actor=request.user,
project_id=project_id,
comment=f"{request.user.email} created the issue from {page_block.name} block",
verb="created",
)
page_block.issue = issue
page_block.save()
return Response(IssueLiteSerializer(issue).data, status=status.HTTP_200_OK)
except PageBlock.DoesNotExist:
return Response(
{"error": "Page Block does not exist"}, status=status.HTTP_404_NOT_FOUND
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class RecentPagesEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def get(self, request, slug, project_id):
try:
subquery = PageFavorite.objects.filter(
user=request.user,
page_id=OuterRef("pk"),
project_id=project_id,
workspace__slug=slug,
)
current_time = date.today()
day_before = current_time - timedelta(days=1)
todays_pages = (
Page.objects.filter(
updated_at__date=date.today(),
workspace__slug=slug,
project_id=project_id,
)
.filter(project__project_projectmember__member=request.user)
.annotate(is_favorite=Exists(subquery))
.filter(Q(owned_by=self.request.user) | Q(access=0))
.select_related("project")
.select_related("workspace")
.select_related("owned_by")
.prefetch_related("labels")
.prefetch_related(
Prefetch(
"blocks",
queryset=PageBlock.objects.select_related(
"page", "issue", "workspace", "project"
),
)
)
.order_by("-is_favorite", "-updated_at")
)
yesterdays_pages = (
Page.objects.filter(
updated_at__date=day_before,
workspace__slug=slug,
project_id=project_id,
)
.filter(project__project_projectmember__member=request.user)
.annotate(is_favorite=Exists(subquery))
.filter(Q(owned_by=self.request.user) | Q(access=0))
.select_related("project")
.select_related("workspace")
.select_related("owned_by")
.prefetch_related("labels")
.prefetch_related(
Prefetch(
"blocks",
queryset=PageBlock.objects.select_related(
"page", "issue", "workspace", "project"
),
)
)
.order_by("-is_favorite", "-updated_at")
)
earlier_this_week = (
Page.objects.filter(
updated_at__date__range=(
(timezone.now() - timedelta(days=7)),
(timezone.now() - timedelta(days=2)),
),
workspace__slug=slug,
project_id=project_id,
)
.annotate(is_favorite=Exists(subquery))
.filter(Q(owned_by=self.request.user) | Q(access=0))
.filter(project__project_projectmember__member=request.user)
.annotate(is_favorite=Exists(subquery))
.select_related("project")
.select_related("workspace")
.select_related("owned_by")
.prefetch_related("labels")
.prefetch_related(
Prefetch(
"blocks",
queryset=PageBlock.objects.select_related(
"page", "issue", "workspace", "project"
),
)
)
.order_by("-is_favorite", "-updated_at")
)
todays_pages_serializer = PageSerializer(todays_pages, many=True)
yesterday_pages_serializer = PageSerializer(yesterdays_pages, many=True)
earlier_this_week_serializer = PageSerializer(earlier_this_week, many=True)
return Response(
{
"today": todays_pages_serializer.data,
"yesterday": yesterday_pages_serializer.data,
"earlier_this_week": earlier_this_week_serializer.data,
},
status=status.HTTP_200_OK,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class FavoritePagesEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def get(self, request, slug, project_id):
try:
subquery = PageFavorite.objects.filter(
user=request.user,
page_id=OuterRef("pk"),
project_id=project_id,
workspace__slug=slug,
)
pages = (
Page.objects.filter(
workspace__slug=slug,
project_id=project_id,
)
.annotate(is_favorite=Exists(subquery))
.filter(Q(owned_by=self.request.user) | Q(access=0))
.filter(project__project_projectmember__member=request.user)
.filter(is_favorite=True)
.select_related("project")
.select_related("workspace")
.select_related("owned_by")
.prefetch_related("labels")
.prefetch_related(
Prefetch(
"blocks",
queryset=PageBlock.objects.select_related(
"page", "issue", "workspace", "project"
),
)
)
.order_by("name", "-is_favorite")
)
serializer = PageSerializer(pages, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class MyPagesEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def get(self, request, slug, project_id):
try:
subquery = PageFavorite.objects.filter(
user=request.user,
page_id=OuterRef("pk"),
project_id=project_id,
workspace__slug=slug,
)
pages = (
Page.objects.filter(
workspace__slug=slug, project_id=project_id, owned_by=request.user
)
.select_related("project")
.select_related("workspace")
.select_related("owned_by")
.prefetch_related("labels")
.annotate(is_favorite=Exists(subquery))
.filter(Q(owned_by=self.request.user) | Q(access=0))
.filter(project__project_projectmember__member=request.user)
.prefetch_related(
Prefetch(
"blocks",
queryset=PageBlock.objects.select_related(
"page", "issue", "workspace", "project"
),
)
)
.order_by("-is_favorite", "name")
)
serializer = PageSerializer(pages, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class CreatedbyOtherPagesEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def get(self, request, slug, project_id):
try:
subquery = PageFavorite.objects.filter(
user=request.user,
page_id=OuterRef("pk"),
project_id=project_id,
workspace__slug=slug,
)
pages = (
Page.objects.filter(
~Q(owned_by=request.user),
workspace__slug=slug,
project_id=project_id,
access=0,
)
.select_related("project")
.select_related("workspace")
.select_related("owned_by")
.prefetch_related("labels")
.annotate(is_favorite=Exists(subquery))
.prefetch_related(
Prefetch(
"blocks",
queryset=PageBlock.objects.select_related(
"page", "issue", "workspace", "project"
),
)
)
.order_by("-is_favorite", "name")
)
serializer = PageSerializer(pages, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
+72 -3
View File
@@ -7,10 +7,20 @@ from sentry_sdk import capture_exception
# Module imports
from plane.api.serializers import (
UserSerializer,
IssueActivitySerializer,
)
from plane.api.views.base import BaseViewSet, BaseAPIView
from plane.db.models import User, Workspace
from plane.db.models import (
User,
Workspace,
WorkspaceMemberInvite,
Issue,
IssueActivity,
WorkspaceMember,
)
from plane.utils.paginator import BasePaginator
class UserEndpoint(BaseViewSet):
serializer_class = UserSerializer
@@ -22,11 +32,34 @@ class UserEndpoint(BaseViewSet):
def retrieve(self, request):
try:
workspace = Workspace.objects.get(pk=request.user.last_workspace_id)
workspace_invites = WorkspaceMemberInvite.objects.filter(
email=request.user.email
).count()
assigned_issues = Issue.objects.filter(assignees__in=[request.user]).count()
return Response(
{"user": UserSerializer(request.user).data, "slug": workspace.slug}
{
"user": UserSerializer(request.user).data,
"slug": workspace.slug,
"workspace_invites": workspace_invites,
"assigned_issues": assigned_issues,
},
status=status.HTTP_200_OK,
)
except Workspace.DoesNotExist:
return Response({"user": UserSerializer(request.user).data, "slug": None})
workspace_invites = WorkspaceMemberInvite.objects.filter(
email=request.user.email
).count()
assigned_issues = Issue.objects.filter(assignees__in=[request.user]).count()
return Response(
{
"user": UserSerializer(request.user).data,
"slug": None,
"workspace_invites": workspace_invites,
"assigned_issues": assigned_issues,
},
status=status.HTTP_200_OK,
)
except Exception as e:
return Response(
{"error": "Something went wrong please try again later"},
@@ -40,6 +73,20 @@ class UpdateUserOnBoardedEndpoint(BaseAPIView):
user = User.objects.get(pk=request.user.id)
user.is_onboarded = request.data.get("is_onboarded", False)
user.save()
if user.last_workspace_id is not None:
user_role = WorkspaceMember.objects.filter(
workspace_id=user.last_workspace_id, member=request.user.id
).first()
return Response(
{
"message": "Updated successfully",
"role": user_role.company_role
if user_role is not None
else None,
},
status=status.HTTP_200_OK,
)
return Response(
{"message": "Updated successfully"}, status=status.HTTP_200_OK
)
@@ -49,3 +96,25 @@ class UpdateUserOnBoardedEndpoint(BaseAPIView):
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class UserActivityEndpoint(BaseAPIView, BasePaginator):
def get(self, request):
try:
queryset = IssueActivity.objects.filter(actor=request.user).select_related(
"actor", "workspace"
)
return self.paginate(
request=request,
queryset=queryset,
on_results=lambda issue_activities: IssueActivitySerializer(
issue_activities, many=True
).data,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
+79 -2
View File
@@ -5,7 +5,7 @@ from datetime import datetime
# Django imports
from django.core.exceptions import ValidationError
from django.db import IntegrityError
from django.db.models import Q, Exists, OuterRef
from django.db.models import Q, Exists, OuterRef, Func, F
from django.core.validators import validate_email
from django.conf import settings
@@ -46,6 +46,8 @@ from plane.db.models import (
ProjectMemberInvite,
User,
ProjectIdentifier,
Cycle,
Module,
)
from plane.bgtasks.project_invitation_task import project_invitation
@@ -64,6 +66,11 @@ class ProjectViewSet(BaseViewSet):
return ProjectDetailSerializer
def get_queryset(self):
subquery = ProjectFavorite.objects.filter(
user=self.request.user,
project_id=OuterRef("pk"),
workspace__slug=self.kwargs.get("slug"),
)
return self.filter_queryset(
super()
.get_queryset()
@@ -72,6 +79,7 @@ class ProjectViewSet(BaseViewSet):
.select_related(
"workspace", "workspace__owner", "default_assignee", "project_lead"
)
.annotate(is_favorite=Exists(subquery))
.distinct()
)
@@ -82,7 +90,31 @@ class ProjectViewSet(BaseViewSet):
project_id=OuterRef("pk"),
workspace__slug=self.kwargs.get("slug"),
)
projects = self.get_queryset().annotate(is_favorite=Exists(subquery))
projects = (
self.get_queryset()
.annotate(is_favorite=Exists(subquery))
.order_by("-is_favorite", "name")
.annotate(
total_members=ProjectMember.objects.filter(
project_id=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
total_cycles=Cycle.objects.filter(project_id=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
total_modules=Module.objects.filter(project_id=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
return Response(ProjectDetailSerializer(projects, many=True).data)
except Exception as e:
capture_exception(e)
@@ -151,6 +183,7 @@ class ProjectViewSet(BaseViewSet):
workspace=serializer.instance.workspace,
group=state["group"],
default=state.get("default", False),
created_by=request.user,
)
for state in states
]
@@ -167,6 +200,12 @@ class ProjectViewSet(BaseViewSet):
{"name": "The project name is already taken"},
status=status.HTTP_410_GONE,
)
else:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_410_GONE,
)
except Workspace.DoesNotExist as e:
return Response(
{"error": "Workspace does not exist"}, status=status.HTTP_404_NOT_FOUND
@@ -328,6 +367,7 @@ class UserProjectInvitationsViewset(BaseViewSet):
workspace=invitation.project.workspace,
member=request.user,
role=invitation.role,
created_by=request.user,
)
for invitation in project_invitations
]
@@ -369,6 +409,41 @@ class ProjectMemberViewSet(BaseViewSet):
.select_related("workspace", "workspace__owner")
)
def partial_update(self, request, slug, project_id, pk):
try:
project_member = ProjectMember.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
if request.user.id == project_member.member_id:
return Response(
{"error": "You cannot update your own role"},
status=status.HTTP_400_BAD_REQUEST,
)
if request.data.get("role", 10) > project_member.role:
return Response(
{
"error": "You cannot update a role that is higher than your own role"
},
status=status.HTTP_400_BAD_REQUEST,
)
serializer = ProjectMemberSerializer(
project_member, data=request.data, partial=True
)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except ProjectMember.DoesNotExist:
return Response(
{"error": "Project Member does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST)
class AddMemberToProjectEndpoint(BaseAPIView):
permission_classes = [
@@ -449,6 +524,7 @@ class AddTeamToProjectEndpoint(BaseAPIView):
project_id=project_id,
member_id=member,
workspace=workspace,
created_by=request.user,
)
)
@@ -596,6 +672,7 @@ class ProjectJoinEndpoint(BaseAPIView):
if workspace_role >= 15
else (15 if workspace_role == 10 else workspace_role),
workspace=workspace,
created_by=request.user,
)
for project_id in project_ids
],
+21
View File
@@ -0,0 +1,21 @@
# 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 BaseAPIView
from plane.utils.integrations.github import get_release_notes
class ReleaseNotesEndpoint(BaseAPIView):
def get(self, request):
try:
release_notes = get_release_notes()
return Response(release_notes, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
+252
View File
@@ -0,0 +1,252 @@
# Python imports
import re
# Django imports
from django.db.models import Q
# 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 BaseAPIView
from plane.db.models import Workspace, Project, Issue, Cycle, Module, Page, IssueView
from plane.utils.issue_search import search_issues
class GlobalSearchEndpoint(BaseAPIView):
"""Endpoint to search across multiple fields in the workspace and
also show related workspace if found
"""
def filter_workspaces(self, query, slug, project_id):
fields = ["name"]
q = Q()
for field in fields:
q |= Q(**{f"{field}__icontains": query})
return (
Workspace.objects.filter(q, workspace_member__member=self.request.user)
.distinct()
.values("name", "id", "slug")
)
def filter_projects(self, query, slug, project_id):
fields = ["name"]
q = Q()
for field in fields:
q |= Q(**{f"{field}__icontains": query})
return (
Project.objects.filter(
q,
Q(project_projectmember__member=self.request.user) | Q(network=2),
workspace__slug=slug,
)
.distinct()
.values("name", "id", "identifier", "workspace__slug")
)
def filter_issues(self, query, slug, project_id):
fields = ["name", "sequence_id"]
q = Q()
for field in fields:
if field == "sequence_id":
sequences = re.findall(r"\d+\.\d+|\d+", query)
for sequence_id in sequences:
q |= Q(**{"sequence_id": sequence_id})
else:
q |= Q(**{f"{field}__icontains": query})
return (
Issue.objects.filter(
q,
project__project_projectmember__member=self.request.user,
workspace__slug=slug,
project_id=project_id,
)
.distinct()
.values(
"name",
"id",
"sequence_id",
"project__identifier",
"project_id",
"workspace__slug",
)
)
def filter_cycles(self, query, slug, project_id):
fields = ["name"]
q = Q()
for field in fields:
q |= Q(**{f"{field}__icontains": query})
return (
Cycle.objects.filter(
q,
project__project_projectmember__member=self.request.user,
workspace__slug=slug,
project_id=project_id,
)
.distinct()
.values(
"name",
"id",
"project_id",
"workspace__slug",
)
)
def filter_modules(self, query, slug, project_id):
fields = ["name"]
q = Q()
for field in fields:
q |= Q(**{f"{field}__icontains": query})
return (
Module.objects.filter(
q,
project__project_projectmember__member=self.request.user,
workspace__slug=slug,
project_id=project_id,
)
.distinct()
.values(
"name",
"id",
"project_id",
"workspace__slug",
)
)
def filter_pages(self, query, slug, project_id):
fields = ["name"]
q = Q()
for field in fields:
q |= Q(**{f"{field}__icontains": query})
return (
Page.objects.filter(
q,
project__project_projectmember__member=self.request.user,
workspace__slug=slug,
project_id=project_id,
)
.distinct()
.values(
"name",
"id",
"project_id",
"workspace__slug",
)
)
def filter_views(self, query, slug, project_id):
fields = ["name"]
q = Q()
for field in fields:
q |= Q(**{f"{field}__icontains": query})
return (
IssueView.objects.filter(
q,
project__project_projectmember__member=self.request.user,
workspace__slug=slug,
project_id=project_id,
)
.distinct()
.values(
"name",
"id",
"project_id",
"workspace__slug",
)
)
def get(self, request, slug, project_id):
try:
query = request.query_params.get("search", False)
if not query:
return Response(
{
"results": {
"workspace": [],
"project": [],
"issue": [],
"cycle": [],
"module": [],
"issue_view": [],
"page": [],
}
},
status=status.HTTP_200_OK,
)
MODELS_MAPPER = {
"workspace": self.filter_workspaces,
"project": self.filter_projects,
"issue": self.filter_issues,
"cycle": self.filter_cycles,
"module": self.filter_modules,
"issue_view": self.filter_views,
"page": self.filter_pages,
}
results = {}
for model in MODELS_MAPPER.keys():
func = MODELS_MAPPER.get(model, None)
results[model] = func(query, slug, project_id)
return Response({"results": results}, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class IssueSearchEndpoint(BaseAPIView):
def get(self, request, slug, project_id):
try:
query = request.query_params.get("search", False)
parent = request.query_params.get("parent", False)
blocker_blocked_by = request.query_params.get("blocker_blocked_by", False)
issue_id = request.query_params.get("issue_id", False)
issues = search_issues(query)
issues = issues.filter(
workspace__slug=slug,
project_id=project_id,
project__project_projectmember__member=self.request.user,
)
if parent == "true" and issue_id:
issue = Issue.objects.get(pk=issue_id)
issues = issues.filter(
~Q(pk=issue_id), ~Q(pk=issue.parent_id), parent__isnull=True
).exclude(
pk__in=Issue.objects.filter(parent__isnull=False).values_list(
"parent_id", flat=True
)
)
if blocker_blocked_by == "true" and issue_id:
issues = issues.filter(blocker_issues=issue_id, blocked_issues=issue_id)
return Response(
issues.values(
"name",
"id",
"sequence_id",
"project__identifier",
"project_id",
"workspace__slug",
),
status=status.HTTP_200_OK,
)
except Issue.DoesNotExist:
return Response(
{"error": "Issue Does not exist"}, status=status.HTTP_400_BAD_REQUEST
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
+35 -2
View File
@@ -1,6 +1,9 @@
# Python imports
from itertools import groupby
# Django imports
from django.db import IntegrityError
# Third party imports
from rest_framework.response import Response
from rest_framework import status
@@ -8,10 +11,10 @@ from sentry_sdk import capture_exception
# Module imports
from . import BaseViewSet
from . import BaseViewSet, BaseAPIView
from plane.api.serializers import StateSerializer
from plane.api.permissions import ProjectEntityPermission
from plane.db.models import State
from plane.db.models import State, Issue
class StateViewSet(BaseViewSet):
@@ -36,6 +39,25 @@ class StateViewSet(BaseViewSet):
.distinct()
)
def create(self, request, slug, project_id):
try:
serializer = StateSerializer(data=request.data)
if serializer.is_valid():
serializer.save(project_id=project_id)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError:
return Response(
{"error": "State with the name already exists"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def list(self, request, slug, project_id):
try:
state_dict = dict()
@@ -66,6 +88,17 @@ class StateViewSet(BaseViewSet):
{"error": "Default state cannot be deleted"}, status=False
)
# Check for any issues in the state
issue_exist = Issue.objects.filter(state=pk).exists()
if issue_exist:
return Response(
{
"error": "The state is not empty, only empty states can be deleted"
},
status=status.HTTP_400_BAD_REQUEST,
)
state.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except State.DoesNotExist:
+133 -7
View File
@@ -1,14 +1,31 @@
# Django imports
from django.db import IntegrityError
from django.db.models import Prefetch, OuterRef, Exists
# Third party imports
from rest_framework.response import Response
from rest_framework import status
from sentry_sdk import capture_exception
# Module imports
from . import BaseViewSet
from plane.api.serializers import ViewSerializer
from . import BaseViewSet, BaseAPIView
from plane.api.serializers import (
IssueViewSerializer,
IssueLiteSerializer,
IssueViewFavoriteSerializer,
)
from plane.api.permissions import ProjectEntityPermission
from plane.db.models import View
from plane.db.models import (
IssueView,
Issue,
IssueViewFavorite,
)
from plane.utils.issue_filters import issue_filters
class ViewViewSet(BaseViewSet):
serializer_class = ViewSerializer
model = View
class IssueViewViewSet(BaseViewSet):
serializer_class = IssueViewSerializer
model = IssueView
permission_classes = [
ProjectEntityPermission,
]
@@ -17,6 +34,12 @@ class ViewViewSet(BaseViewSet):
serializer.save(project_id=self.kwargs.get("project_id"))
def get_queryset(self):
subquery = IssueViewFavorite.objects.filter(
user=self.request.user,
view_id=OuterRef("pk"),
project_id=self.kwargs.get("project_id"),
workspace__slug=self.kwargs.get("slug"),
)
return self.filter_queryset(
super()
.get_queryset()
@@ -25,5 +48,108 @@ class ViewViewSet(BaseViewSet):
.filter(project__project_projectmember__member=self.request.user)
.select_related("project")
.select_related("workspace")
.annotate(is_favorite=Exists(subquery))
.order_by("-is_favorite", "name")
.distinct()
)
class ViewIssuesEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def get(self, request, slug, project_id, view_id):
try:
view = IssueView.objects.get(pk=view_id)
queries = view.query
filters = issue_filters(request.query_params, "GET")
issues = (
Issue.objects.filter(
**queries, project_id=project_id, workspace__slug=slug
)
.filter(**filters)
.select_related("project")
.select_related("workspace")
.select_related("state")
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
)
serializer = IssueLiteSerializer(issues, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
except IssueView.DoesNotExist:
return Response(
{"error": "Issue View does not exist"}, status=status.HTTP_404_NOT_FOUND
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class IssueViewFavoriteViewSet(BaseViewSet):
serializer_class = IssueViewFavoriteSerializer
model = IssueViewFavorite
def get_queryset(self):
return self.filter_queryset(
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(user=self.request.user)
.select_related("view")
)
def create(self, request, slug, project_id):
try:
serializer = IssueViewFavoriteSerializer(data=request.data)
if serializer.is_valid():
serializer.save(user=request.user, project_id=project_id)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError as e:
if "already exists" in str(e):
return Response(
{"error": "The view is already added to favorites"},
status=status.HTTP_410_GONE,
)
else:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def destroy(self, request, slug, project_id, view_id):
try:
view_favourite = IssueViewFavorite.objects.get(
project=project_id,
user=request.user,
workspace__slug=slug,
view_id=view_id,
)
view_favourite.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except IssueViewFavorite.DoesNotExist:
return Response(
{"error": "View is not in favorites"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
+250 -6
View File
@@ -1,6 +1,7 @@
# Python imports
import jwt
from datetime import datetime
from datetime import date, datetime
from dateutil.relativedelta import relativedelta
# Django imports
from django.db import IntegrityError
@@ -10,8 +11,16 @@ from django.utils import timezone
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
from django.contrib.sites.shortcuts import get_current_site
from django.db.models import CharField, Count, OuterRef, Func, F
from django.db.models.functions import Cast
from django.db.models import (
CharField,
Count,
OuterRef,
Func,
F,
Q,
)
from django.db.models.functions import ExtractWeek, Cast, ExtractDay
from django.db.models.fields import DateField
# Third party modules
from rest_framework import status
@@ -27,6 +36,7 @@ from plane.api.serializers import (
WorkSpaceMemberInviteSerializer,
UserLiteSerializer,
ProjectMemberSerializer,
WorkspaceThemeSerializer,
)
from plane.api.views.base import BaseAPIView
from . import BaseViewSet
@@ -37,6 +47,9 @@ from plane.db.models import (
WorkspaceMemberInvite,
Team,
ProjectMember,
IssueActivity,
Issue,
WorkspaceTheme,
)
from plane.api.permissions import WorkSpaceBasePermission, WorkSpaceAdminPermission
from plane.bgtasks.workspace_invitation_task import workspace_invitation
@@ -59,7 +72,9 @@ class WorkSpaceViewSet(BaseViewSet):
lookup_field = "slug"
def get_queryset(self):
return self.filter_queryset(super().get_queryset().select_related("owner"))
return self.filter_queryset(
super().get_queryset().select_related("owner")
).order_by("name")
def create(self, request):
try:
@@ -130,7 +145,6 @@ class UserWorkSpacesEndpoint(BaseAPIView):
return Response(serializer.data, status=status.HTTP_200_OK)
except Exception as e:
print(e)
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
@@ -209,6 +223,7 @@ class InviteWorkspaceEndpoint(BaseAPIView):
algorithm="HS256",
),
role=email.get("role", 10),
created_by=request.user,
)
)
except ValidationError:
@@ -318,7 +333,6 @@ class JoinWorkspaceEndpoint(BaseAPIView):
status=status.HTTP_404_NOT_FOUND,
)
except Exception as e:
print(e)
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
@@ -368,6 +382,7 @@ class UserWorkspaceInvitationsEndpoint(BaseViewSet):
workspace=invitation.workspace,
member=request.user,
role=invitation.role,
created_by=request.user,
)
for invitation in workspace_invitations
],
@@ -408,6 +423,43 @@ class WorkSpaceMemberViewSet(BaseViewSet):
.select_related("member")
)
def partial_update(self, request, slug, pk):
try:
workspace_member = WorkspaceMember.objects.get(pk=pk, workspace__slug=slug)
if request.user.id == workspace_member.member_id:
return Response(
{"error": "You cannot update your own role"},
status=status.HTTP_400_BAD_REQUEST,
)
if request.data.get("role", 10) > workspace_member.role:
return Response(
{
"error": "You cannot update a role that is higher than your own role"
},
status=status.HTTP_400_BAD_REQUEST,
)
serializer = WorkSpaceMemberSerializer(
workspace_member, data=request.data, partial=True
)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except WorkspaceMember.DoesNotExist:
return Response(
{"error": "Workspace Member does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class TeamMemberViewSet(BaseViewSet):
serializer_class = TeamSerializer
@@ -578,3 +630,195 @@ class WorkspaceMemberUserViewsEndpoint(BaseAPIView):
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class UserActivityGraphEndpoint(BaseAPIView):
def get(self, request, slug):
try:
issue_activities = (
IssueActivity.objects.filter(
actor=request.user,
workspace__slug=slug,
created_at__date__gte=date.today() + relativedelta(months=-6),
)
.annotate(created_date=Cast("created_at", DateField()))
.values("created_date")
.annotate(activity_count=Count("created_date"))
.order_by("created_date")
)
return Response(issue_activities, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class UserIssueCompletedGraphEndpoint(BaseAPIView):
def get(self, request, slug):
try:
month = request.GET.get("month", 1)
issues = (
Issue.objects.filter(
assignees__in=[request.user],
workspace__slug=slug,
completed_at__month=month,
completed_at__isnull=False,
)
.annotate(completed_week=ExtractWeek("completed_at"))
.annotate(week=F("completed_week") % 4)
.values("week")
.annotate(completed_count=Count("completed_week"))
.order_by("week")
)
return Response(issues, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class WeekInMonth(Func):
function = "FLOOR"
template = "(((%(expressions)s - 1) / 7) + 1)::INTEGER"
class UserWorkspaceDashboardEndpoint(BaseAPIView):
def get(self, request, slug):
try:
issue_activities = (
IssueActivity.objects.filter(
actor=request.user,
workspace__slug=slug,
created_at__date__gte=date.today() + relativedelta(months=-3),
)
.annotate(created_date=Cast("created_at", DateField()))
.values("created_date")
.annotate(activity_count=Count("created_date"))
.order_by("created_date")
)
month = request.GET.get("month", 1)
completed_issues = (
Issue.objects.filter(
assignees__in=[request.user],
workspace__slug=slug,
completed_at__month=month,
completed_at__isnull=False,
)
.annotate(day_of_month=ExtractDay("completed_at"))
.annotate(week_in_month=WeekInMonth(F("day_of_month")))
.values("week_in_month")
.annotate(completed_count=Count("id"))
.order_by("week_in_month")
)
assigned_issues = Issue.objects.filter(
workspace__slug=slug, assignees__in=[request.user]
).count()
pending_issues_count = Issue.objects.filter(
~Q(state__group__in=["completed", "cancelled"]),
workspace__slug=slug,
assignees__in=[request.user],
).count()
completed_issues_count = Issue.objects.filter(
workspace__slug=slug,
assignees__in=[request.user],
state__group="completed",
).count()
issues_due_week = (
Issue.objects.filter(
workspace__slug=slug,
assignees__in=[request.user],
)
.annotate(target_week=ExtractWeek("target_date"))
.filter(target_week=timezone.now().date().isocalendar()[1])
.count()
)
state_distribution = (
Issue.objects.filter(workspace__slug=slug, assignees__in=[request.user])
.annotate(state_group=F("state__group"))
.values("state_group")
.annotate(state_count=Count("state_group"))
.order_by("state_group")
)
overdue_issues = Issue.objects.filter(
~Q(state__group__in=["completed", "cancelled"]),
workspace__slug=slug,
assignees__in=[request.user],
target_date__lt=timezone.now(),
completed_at__isnull=True,
).values("id", "name", "workspace__slug", "project_id", "target_date")
upcoming_issues = Issue.objects.filter(
~Q(state__group__in=["completed", "cancelled"]),
target_date__gte=timezone.now(),
workspace__slug=slug,
assignees__in=[request.user],
completed_at__isnull=True,
).values("id", "name", "workspace__slug", "project_id", "target_date")
return Response(
{
"issue_activities": issue_activities,
"completed_issues": completed_issues,
"assigned_issues_count": assigned_issues,
"pending_issues_count": pending_issues_count,
"completed_issues_count": completed_issues_count,
"issues_due_week_count": issues_due_week,
"state_distribution": state_distribution,
"overdue_issues": overdue_issues,
"upcoming_issues": upcoming_issues,
},
status=status.HTTP_200_OK,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class WorkspaceThemeViewSet(BaseViewSet):
permission_classes = [
WorkSpaceAdminPermission,
]
model = WorkspaceTheme
serializer_class = WorkspaceThemeSerializer
def get_queryset(self):
return super().get_queryset().filter(workspace__slug=self.kwargs.get("slug"))
def create(self, request, slug):
try:
workspace = Workspace.objects.get(slug=slug)
serializer = WorkspaceThemeSerializer(data=request.data)
if serializer.is_valid():
serializer.save(workspace=workspace, actor=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Workspace.DoesNotExist:
return Response(
{"error": "Workspace does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -0,0 +1,174 @@
# Python imports
import csv
import io
# Django imports
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
# Third party imports
from celery import shared_task
from sentry_sdk import capture_exception
# Module imports
from plane.db.models import Issue
from plane.utils.analytics_plot import build_graph_plot
from plane.utils.issue_filters import issue_filters
row_mapping = {
"state__name": "State",
"state__group": "State Group",
"labels__name": "Label",
"assignees__email": "Assignee Name",
"start_date": "Start Date",
"target_date": "Due Date",
"completed_at": "Completed At",
"created_at": "Created At",
"issue_count": "Issue Count",
"priority": "Priority",
"estimate": "Estimate",
}
@shared_task
def analytic_export_task(email, data, slug):
try:
filters = issue_filters(data, "POST")
queryset = Issue.objects.filter(**filters, workspace__slug=slug)
x_axis = data.get("x_axis", False)
y_axis = data.get("y_axis", False)
segment = data.get("segment", False)
distribution = build_graph_plot(
queryset=queryset, x_axis=x_axis, y_axis=y_axis, segment=segment
)
key = "count" if y_axis == "issue_count" else "estimate"
segmented = segment
assignee_details = {}
if x_axis in ["assignees__email"] or segment in ["assignees__email"]:
assignee_details = (
Issue.objects.filter(workspace__slug=slug, **filters, assignees__avatar__isnull=False)
.order_by("assignees__id")
.distinct("assignees__id")
.values("assignees__avatar", "assignees__email", "assignees__first_name", "assignees__last_name")
)
if segment:
segment_zero = []
for item in distribution:
current_dict = distribution.get(item)
for current in current_dict:
segment_zero.append(current.get("segment"))
segment_zero = list(set(segment_zero))
row_zero = (
[
row_mapping.get(x_axis, "X-Axis"),
]
+ [
row_mapping.get(y_axis, "Y-Axis"),
]
+ segment_zero
)
rows = []
for item in distribution:
generated_row = [
item,
]
data = distribution.get(item)
# Add y axis values
generated_row.append(sum(obj.get(key) for obj in data if obj.get(key, None) is not None))
for segment in segment_zero:
value = [x for x in data if x.get("segment") == segment]
if len(value):
generated_row.append(value[0].get(key))
else:
generated_row.append("0")
# x-axis replacement for names
if x_axis in ["assignees__email"]:
assignee = [user for user in assignee_details if str(user.get("assignees__email")) == str(item)]
if len(assignee):
generated_row[0] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name"))
rows.append(tuple(generated_row))
# If segment is ["assignees__email"] then replace segment_zero rows with first and last names
if segmented in ["assignees__email"]:
for index, segm in enumerate(row_zero[2:]):
# find the name of the user
assignee = [user for user in assignee_details if str(user.get("assignees__email")) == str(segm)]
if len(assignee):
row_zero[index] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name"))
rows = [tuple(row_zero)] + rows
csv_buffer = io.StringIO()
writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL)
# Write CSV data to the buffer
for row in rows:
writer.writerow(row)
subject = "Your Export is ready"
html_content = render_to_string("emails/exports/analytics.html", {})
text_content = strip_tags(html_content)
csv_buffer.seek(0)
msg = EmailMultiAlternatives(
subject, text_content, settings.EMAIL_FROM, [email]
)
msg.attach(f"{slug}-analytics.csv", csv_buffer.read())
msg.send(fail_silently=False)
else:
row_zero = [
row_mapping.get(x_axis, "X-Axis"),
row_mapping.get(y_axis, "Y-Axis"),
]
rows = []
for item in distribution:
row = [
item,
distribution.get(item)[0].get("count")
if y_axis == "issue_count"
else distribution.get(item)[0].get("estimate "),
]
# x-axis replacement to names
if x_axis in ["assignees__email"]:
assignee = [user for user in assignee_details if str(user.get("assignees__email")) == str(item)]
if len(assignee):
row[0] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name"))
rows.append(tuple(row))
rows = [tuple(row_zero)] + rows
csv_buffer = io.StringIO()
writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL)
# Write CSV data to the buffer
for row in rows:
writer.writerow(row)
subject = "Your Export is ready"
html_content = render_to_string("emails/exports/analytics.html", {})
text_content = strip_tags(html_content)
csv_buffer.seek(0)
msg = EmailMultiAlternatives(
subject, text_content, settings.EMAIL_FROM, [email]
)
msg.attach(f"{slug}-analytics.csv", csv_buffer.read())
msg.send(fail_silently=False)
except Exception as e:
print(e)
capture_exception(e)
return
@@ -2,23 +2,26 @@
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
# Third party imports
from django_rq import job
from celery import shared_task
from sentry_sdk import capture_exception
# Module imports
from plane.db.models import User
@job("default")
@shared_task
def email_verification(first_name, email, token, current_site):
try:
realtivelink = "/request-email-verification/" + "?token=" + str(token)
abs_url = "http://" + current_site + realtivelink
from_email_string = f"Team Plane <team@mailer.plane.so>"
from_email_string = settings.EMAIL_FROM
subject = f"Verify your Email!"
@@ -2,23 +2,24 @@
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
# Third party imports
from django_rq import job
from celery import shared_task
from sentry_sdk import capture_exception
# Module imports
from plane.db.models import User
@job("default")
@shared_task
def forgot_password(first_name, email, uidb64, token, current_site):
try:
realtivelink = f"/email-verify/?uidb64={uidb64}&token={token}/"
abs_url = "http://" + current_site + realtivelink
from_email_string = f"Team Plane <team@mailer.plane.so>"
from_email_string = settings.EMAIL_FROM
subject = f"Verify your Email!"
+179
View File
@@ -0,0 +1,179 @@
# Python imports
import json
import requests
import uuid
import jwt
from datetime import datetime
# Django imports
from django.conf import settings
from django.core.serializers.json import DjangoJSONEncoder
from django.contrib.auth.hashers import make_password
# Third Party imports
from celery import shared_task
from sentry_sdk import capture_exception
# Module imports
from plane.api.serializers import ImporterSerializer
from plane.db.models import (
Importer,
WorkspaceMember,
GithubRepositorySync,
GithubRepository,
ProjectMember,
WorkspaceIntegration,
Label,
User,
)
from .workspace_invitation_task import workspace_invitation
from plane.bgtasks.user_welcome_task import send_welcome_slack
@shared_task
def service_importer(service, importer_id):
try:
importer = Importer.objects.get(pk=importer_id)
importer.status = "processing"
importer.save()
users = importer.data.get("users", [])
# Check if we need to import users as well
if len(users):
# For all invited users create the users
new_users = User.objects.bulk_create(
[
User(
email=user.get("email").strip().lower(),
username=uuid.uuid4().hex,
password=make_password(uuid.uuid4().hex),
is_password_autoset=True,
)
for user in users
if user.get("import", False) == "invite"
],
batch_size=10,
ignore_conflicts=True,
)
[
send_welcome_slack.delay(
str(user.id),
True,
f"{user.email} was imported to Plane from {service}",
)
for user in new_users
]
workspace_users = User.objects.filter(
email__in=[
user.get("email").strip().lower()
for user in users
if user.get("import", False) == "invite"
or user.get("import", False) == "map"
]
)
# Add new users to Workspace and project automatically
WorkspaceMember.objects.bulk_create(
[
WorkspaceMember(
member=user,
workspace_id=importer.workspace_id,
created_by=importer.created_by,
)
for user in workspace_users
],
batch_size=100,
ignore_conflicts=True,
)
ProjectMember.objects.bulk_create(
[
ProjectMember(
project_id=importer.project_id,
workspace_id=importer.workspace_id,
member=user,
created_by=importer.created_by,
)
for user in workspace_users
],
batch_size=100,
ignore_conflicts=True,
)
# Check if sync config is on for github importers
if service == "github" and importer.config.get("sync", False):
name = importer.metadata.get("name", False)
url = importer.metadata.get("url", False)
config = importer.metadata.get("config", {})
owner = importer.metadata.get("owner", False)
repository_id = importer.metadata.get("repository_id", False)
workspace_integration = WorkspaceIntegration.objects.get(
workspace_id=importer.workspace_id, integration__provider="github"
)
# Delete the old repository object
GithubRepositorySync.objects.filter(project_id=importer.project_id).delete()
GithubRepository.objects.filter(project_id=importer.project_id).delete()
# Create a Label for github
label = Label.objects.filter(
name="GitHub", project_id=importer.project_id
).first()
if label is None:
label = Label.objects.create(
name="GitHub",
project_id=importer.project_id,
description="Label to sync Plane issues with GitHub issues",
color="#003773",
)
# Create repository
repo = GithubRepository.objects.create(
name=name,
url=url,
config=config,
repository_id=repository_id,
owner=owner,
project_id=importer.project_id,
)
# Create repo sync
repo_sync = GithubRepositorySync.objects.create(
repository=repo,
workspace_integration=workspace_integration,
actor=workspace_integration.actor,
credentials=importer.data.get("credentials", {}),
project_id=importer.project_id,
label=label,
)
# Add bot as a member in the project
_ = ProjectMember.objects.get_or_create(
member=workspace_integration.actor,
role=20,
project_id=importer.project_id,
)
if settings.PROXY_BASE_URL:
headers = {"Content-Type": "application/json"}
import_data_json = json.dumps(
ImporterSerializer(importer).data,
cls=DjangoJSONEncoder,
)
res = requests.post(
f"{settings.PROXY_BASE_URL}/hooks/workspaces/{str(importer.workspace_id)}/projects/{str(importer.project_id)}/importers/{str(service)}/",
json=import_data_json,
headers=headers,
)
return
except Exception as e:
importer = Importer.objects.get(pk=importer_id)
importer.status = "failed"
importer.save()
capture_exception(e)
return
+309 -98
View File
@@ -7,7 +7,7 @@ from django.conf import settings
from django.core.serializers.json import DjangoJSONEncoder
# Third Party imports
from django_rq import job
from celery import shared_task
from sentry_sdk import capture_exception
# Module imports
@@ -505,14 +505,181 @@ def track_blockings(
)
def track_cycles(
requested_data,
current_instance,
issue_id,
project,
actor,
issue_activities,
def create_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"{actor.email} created the issue",
verb="created",
actor=actor,
)
)
def track_estimate_points(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
if current_instance.get("estimate_point") != requested_data.get("estimate_point"):
if requested_data.get("estimate_point") == None:
issue_activities.append(
IssueActivity(
issue_id=issue_id,
actor=actor,
verb="updated",
old_value=current_instance.get("estimate_point"),
new_value=requested_data.get("estimate_point"),
field="estimate_point",
project=project,
workspace=project.workspace,
comment=f"{actor.email} updated the estimate point to None",
)
)
else:
issue_activities.append(
IssueActivity(
issue_id=issue_id,
actor=actor,
verb="updated",
old_value=current_instance.get("estimate_point"),
new_value=requested_data.get("estimate_point"),
field="estimate_point",
project=project,
workspace=project.workspace,
comment=f"{actor.email} updated the estimate point to {requested_data.get('estimate_point')}",
)
)
def update_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
ISSUE_ACTIVITY_MAPPER = {
"name": track_name,
"parent": track_parent,
"priority": track_priority,
"state": track_state,
"description": track_description,
"target_date": track_target_date,
"start_date": track_start_date,
"labels_list": track_labels,
"assignees_list": track_assignees,
"blocks_list": track_blocks,
"blockers_list": track_blockings,
"estimate_point": track_estimate_points,
}
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
for key in requested_data:
func = ISSUE_ACTIVITY_MAPPER.get(key, None)
if func is not None:
func(
requested_data,
current_instance,
issue_id,
project,
actor,
issue_activities,
)
def delete_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
issue_activities.append(
IssueActivity(
project=project,
workspace=project.workspace,
comment=f"{actor.email} deleted the issue",
verb="deleted",
actor=actor,
field="issue",
)
)
def create_comment_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"{actor.email} created a comment",
verb="created",
actor=actor,
field="comment",
new_value=requested_data.get("comment_html", ""),
new_identifier=requested_data.get("id", None),
issue_comment_id=requested_data.get("id", None),
)
)
def update_comment_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
if current_instance.get("comment_html") != requested_data.get("comment_html"):
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"{actor.email} updated a comment",
verb="updated",
actor=actor,
field="comment",
old_value=current_instance.get("comment_html", ""),
old_identifier=current_instance.get("id"),
new_value=requested_data.get("comment_html", ""),
new_identifier=current_instance.get("id", None),
issue_comment_id=current_instance.get("id", None),
)
)
def delete_comment_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"{actor.email} deleted the comment",
verb="deleted",
actor=actor,
field="comment",
)
)
def create_cycle_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
# Updated Records:
updated_records = current_instance.get("updated_cycle_issues", [])
created_records = json.loads(current_instance.get("created_cycle_issues", []))
@@ -562,14 +729,43 @@ def track_cycles(
)
def track_modules(
requested_data,
current_instance,
issue_id,
project,
actor,
issue_activities,
def delete_cycle_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
cycle_id = requested_data.get("cycle_id", "")
cycle = Cycle.objects.filter(pk=cycle_id).first()
issues = requested_data.get("issues")
for issue in issues:
issue_activities.append(
IssueActivity(
issue_id=issue,
actor=actor,
verb="deleted",
old_value=cycle.name if cycle is not None else "",
new_value="",
field="cycles",
project=project,
workspace=project.workspace,
comment=f"{actor.email} removed this issue from {cycle.name if cycle is not None else None}",
old_identifier=cycle.id if cycle is not None else None,
)
)
def create_module_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
# Updated Records:
updated_records = current_instance.get("updated_module_issues", [])
created_records = json.loads(current_instance.get("created_module_issues", []))
@@ -618,109 +814,85 @@ def track_modules(
)
def create_issue_activity(
def delete_module_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"{actor.email} created the issue",
verb="created",
actor=actor,
)
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
module_id = requested_data.get("module_id", "")
module = Module.objects.filter(pk=module_id).first()
issues = requested_data.get("issues")
def update_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
ISSUE_ACTIVITY_MAPPER = {
"name": track_name,
"parent": track_parent,
"priority": track_priority,
"state": track_state,
"description": track_description,
"target_date": track_target_date,
"start_date": track_start_date,
"labels_list": track_labels,
"assignees_list": track_assignees,
"blocks_list": track_blocks,
"blockers_list": track_blockings,
"cycles_list": track_cycles,
"modules_list": track_modules,
}
for key in requested_data:
func = ISSUE_ACTIVITY_MAPPER.get(key, None)
if func is not None:
func(
requested_data,
current_instance,
issue_id,
project,
actor,
issue_activities,
for issue in issues:
issue_activities.append(
IssueActivity(
issue_id=issue,
actor=actor,
verb="deleted",
old_value=module.name if module is not None else "",
new_value="",
field="modules",
project=project,
workspace=project.workspace,
comment=f"{actor.email} removed this issue from {module.name if module is not None else None}",
old_identifier=module.id if module is not None else None,
)
)
def create_comment_activity(
def create_link_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"{actor.email} created a comment",
comment=f"{actor.email} created a link",
verb="created",
actor=actor,
field="comment",
new_value=requested_data.get("comment_html"),
new_identifier=requested_data.get("id"),
issue_comment_id=requested_data.get("id", None),
field="link",
new_value=requested_data.get("url", ""),
new_identifier=requested_data.get("id", None),
)
)
def update_comment_activity(
def update_link_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
if current_instance.get("comment_html") != requested_data.get("comment_html"):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
if current_instance.get("url") != requested_data.get("url"):
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"{actor.email} updated a comment",
comment=f"{actor.email} updated a link",
verb="updated",
actor=actor,
field="comment",
old_value=current_instance.get("comment_html"),
field="link",
old_value=current_instance.get("url", ""),
old_identifier=current_instance.get("id"),
new_value=requested_data.get("comment_html"),
new_identifier=current_instance.get("id"),
issue_comment_id=current_instance.get("id"),
new_value=requested_data.get("url", ""),
new_identifier=current_instance.get("id", None),
)
)
def delete_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
issue_activities.append(
IssueActivity(
project=project,
workspace=project.workspace,
comment=f"{actor.email} deleted the issue",
verb="deleted",
actor=actor,
field="issue",
)
)
def delete_comment_activity(
def delete_link_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
issue_activities.append(
@@ -728,32 +900,62 @@ def delete_comment_activity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"{actor.email} deleted the comment",
comment=f"{actor.email} deleted the link",
verb="deleted",
actor=actor,
field="comment",
field="link",
)
)
def create_attachment_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"{actor.email} created an attachment",
verb="created",
actor=actor,
field="attachment",
new_value=current_instance.get("access", ""),
new_identifier=current_instance.get("id", None),
)
)
def delete_attachment_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"{actor.email} deleted the attachment",
verb="deleted",
actor=actor,
field="attachment",
)
)
# Receive message from room group
@job("default")
def issue_activity(event):
@shared_task
def issue_activity(
type, requested_data, current_instance, issue_id, actor_id, project_id
):
try:
issue_activities = []
type = event.get("type")
requested_data = json.loads(event.get("requested_data"))
current_instance = (
json.loads(event.get("current_instance"))
if event.get("current_instance") is not None
else None
)
issue_id = event.get("issue_id", None)
actor_id = event.get("actor_id")
project_id = event.get("project_id")
actor = User.objects.get(pk=actor_id)
project = Project.objects.get(pk=project_id)
ACTIVITY_MAPPER = {
@@ -763,6 +965,15 @@ def issue_activity(event):
"comment.activity.created": create_comment_activity,
"comment.activity.updated": update_comment_activity,
"comment.activity.deleted": delete_comment_activity,
"cycle.activity.created": create_cycle_issue_activity,
"cycle.activity.deleted": delete_cycle_issue_activity,
"module.activity.created": create_module_issue_activity,
"module.activity.deleted": delete_module_issue_activity,
"link.activity.created": create_link_activity,
"link.activity.updated": update_link_activity,
"link.activity.deleted": delete_link_activity,
"attachment.activity.created": create_attachment_activity,
"attachment.activity.deleted": delete_attachment_activity,
}
func = ACTIVITY_MAPPER.get(type)
@@ -2,20 +2,20 @@
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
# Third party imports
from django_rq import job
from celery import shared_task
from sentry_sdk import capture_exception
@job("default")
@shared_task
def magic_link(email, key, token, current_site):
try:
realtivelink = f"/magic-sign-in/?password={token}&key={key}"
abs_url = "http://" + current_site + realtivelink
from_email_string = f"Team Plane <team@mailer.plane.so>"
from_email_string = settings.EMAIL_FROM
subject = f"Login for Plane"
@@ -30,6 +30,5 @@ def magic_link(email, key, token, current_site):
msg.send()
return
except Exception as e:
print(e)
capture_exception(e)
return
@@ -2,20 +2,19 @@
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
# Third party imports
from django_rq import job
from celery import shared_task
from sentry_sdk import capture_exception
# Module imports
from plane.db.models import Project, User, ProjectMemberInvite
@job("default")
@shared_task
def project_invitation(email, project_id, token, current_site):
try:
project = Project.objects.get(pk=project_id)
project_member_invite = ProjectMemberInvite.objects.get(
token=token, email=email
@@ -24,7 +23,7 @@ def project_invitation(email, project_id, token, current_site):
relativelink = f"/project-member-invitation/{project_member_invite.id}"
abs_url = "http://" + current_site + relativelink
from_email_string = f"Team Plane <team@mailer.plane.so>"
from_email_string = settings.EMAIL_FROM
subject = f"{project.created_by.first_name or project.created_by.email} invited you to join {project.name} on Plane"
@@ -35,7 +34,9 @@ def project_invitation(email, project_id, token, current_site):
"invitation_url": abs_url,
}
html_content = render_to_string("emails/invitations/project_invitation.html", context)
html_content = render_to_string(
"emails/invitations/project_invitation.html", context
)
text_content = strip_tags(html_content)
@@ -49,6 +50,5 @@ def project_invitation(email, project_id, token, current_site):
except (Project.DoesNotExist, ProjectMemberInvite.DoesNotExist) as e:
return
except Exception as e:
print(e)
capture_exception(e)
return
@@ -0,0 +1,33 @@
# Django imports
from django.conf import settings
# Third party imports
from celery import shared_task
from sentry_sdk import capture_exception
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
# Module imports
from plane.db.models import User
@shared_task
def send_welcome_slack(user_id, created, message):
try:
instance = User.objects.get(pk=user_id)
if created and not instance.is_bot:
# Send message on slack as well
if settings.SLACK_BOT_TOKEN:
client = WebClient(token=settings.SLACK_BOT_TOKEN)
try:
_ = client.chat_postMessage(
channel="#trackers",
text=message,
)
except SlackApiError as e:
print(f"Got an error: {e.response['error']}")
return
except Exception as e:
capture_exception(e)
return
@@ -2,20 +2,21 @@
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
# Third party imports
from django_rq import job
from celery import shared_task
from sentry_sdk import capture_exception
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
# Module imports
from plane.db.models import Workspace, User, WorkspaceMemberInvite
@job("default")
@shared_task
def workspace_invitation(email, workspace_id, token, current_site, invitor):
try:
workspace = Workspace.objects.get(pk=workspace_id)
workspace_member_invite = WorkspaceMemberInvite.objects.get(
token=token, email=email
@@ -26,7 +27,7 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
)
abs_url = "http://" + current_site + realtivelink
from_email_string = f"Team Plane <team@mailer.plane.so>"
from_email_string = settings.EMAIL_FROM
subject = f"{invitor or email} invited you to join {workspace.name} on Plane"
@@ -49,6 +50,18 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
msg = EmailMultiAlternatives(subject, text_content, from_email_string, [email])
msg.attach_alternative(html_content, "text/html")
msg.send()
# Send message on slack as well
if settings.SLACK_BOT_TOKEN:
client = WebClient(token=settings.SLACK_BOT_TOKEN)
try:
_ = client.chat_postMessage(
channel="#trackers",
text=f"{workspace_member_invite.email} has been invited to {workspace.name} as a {workspace_member_invite.role}",
)
except SlackApiError as e:
print(f"Got an error: {e.response['error']}")
return
except (Workspace.DoesNotExist, WorkspaceMemberInvite.DoesNotExist) as e:
return
+17
View File
@@ -0,0 +1,17 @@
import os
from celery import Celery
from plane.settings.redis import redis_instance
# Set the default Django settings module for the 'celery' program.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production")
ri = redis_instance()
app = Celery("plane")
# Using a string here means the worker will not have to
# pickle the object when using Windows.
app.config_from_object("django.conf:settings", namespace="CELERY")
# Load task modules from all registered Django app configs.
app.autodiscover_tasks()
@@ -0,0 +1,92 @@
# Generated by Django 3.2.16 on 2023-03-15 19:10
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('db', '0022_auto_20230307_0304'),
]
operations = [
migrations.CreateModel(
name='Importer',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('service', models.CharField(choices=[('github', 'GitHub')], max_length=50)),
('status', models.CharField(choices=[('queued', 'Queued'), ('processing', 'Processing'), ('completed', 'Completed'), ('failed', 'Failed')], default='queued', max_length=50)),
('metadata', models.JSONField(default=dict)),
('config', models.JSONField(default=dict)),
('data', models.JSONField(default=dict)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='importer_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('initiated_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='imports', to=settings.AUTH_USER_MODEL)),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_importer', to='db.project')),
('token', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='importer', to='db.apitoken')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='importer_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_importer', to='db.workspace')),
],
options={
'verbose_name': 'Importer',
'verbose_name_plural': 'Importers',
'db_table': 'importers',
'ordering': ('-created_at',),
},
),
migrations.CreateModel(
name='IssueView',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('name', models.CharField(max_length=255, verbose_name='View Name')),
('description', models.TextField(blank=True, verbose_name='View Description')),
('query', models.JSONField(verbose_name='View Query')),
('access', models.PositiveSmallIntegerField(choices=[(0, 'Private'), (1, 'Public')], default=1)),
('query_data', models.JSONField(default=dict)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issueview_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_issueview', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issueview_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_issueview', to='db.workspace')),
],
options={
'verbose_name': 'Issue View',
'verbose_name_plural': 'Issue Views',
'db_table': 'issue_views',
'ordering': ('-created_at',),
},
),
migrations.CreateModel(
name='IssueViewFavorite',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issueviewfavorite_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_issueviewfavorite', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issueviewfavorite_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_view_favorites', to=settings.AUTH_USER_MODEL)),
('view', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='view_favorites', to='db.issueview')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_issueviewfavorite', to='db.workspace')),
],
options={
'verbose_name': 'View Favorite',
'verbose_name_plural': 'View Favorites',
'db_table': 'view_favorites',
'ordering': ('-created_at',),
'unique_together': {('view', 'user')},
},
),
migrations.AlterUniqueTogether(
name='label',
unique_together={('name', 'project')},
),
migrations.DeleteModel(
name='View',
),
]
@@ -0,0 +1,113 @@
# Generated by Django 3.2.16 on 2023-03-21 20:08
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('db', '0023_auto_20230316_0040'),
]
operations = [
migrations.CreateModel(
name='Page',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('name', models.CharField(max_length=255)),
('description', models.JSONField(blank=True, default=dict)),
('description_html', models.TextField(blank=True, default='<p></p>')),
('description_stripped', models.TextField(blank=True, null=True)),
('access', models.PositiveSmallIntegerField(choices=[(0, 'Public'), (1, 'Private')], default=0)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='page_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('owned_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pages', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Page',
'verbose_name_plural': 'Pages',
'db_table': 'pages',
'ordering': ('-created_at',),
},
),
migrations.AddField(
model_name='project',
name='issue_views_view',
field=models.BooleanField(default=True),
),
migrations.AlterField(
model_name='importer',
name='service',
field=models.CharField(choices=[('github', 'GitHub'), ('jira', 'Jira')], max_length=50),
),
migrations.AlterField(
model_name='project',
name='cover_image',
field=models.URLField(blank=True, max_length=800, null=True),
),
migrations.CreateModel(
name='PageBlock',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('name', models.CharField(max_length=255)),
('description', models.JSONField(blank=True, default=dict)),
('description_html', models.TextField(blank=True, default='<p></p>')),
('description_stripped', models.TextField(blank=True, null=True)),
('completed_at', models.DateTimeField(null=True)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='pageblock_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('issue', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='blocks', to='db.issue')),
('page', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='blocks', to='db.page')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_pageblock', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='pageblock_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_pageblock', to='db.workspace')),
],
options={
'verbose_name': 'Page Block',
'verbose_name_plural': 'Page Blocks',
'db_table': 'page_blocks',
'ordering': ('-created_at',),
},
),
migrations.AddField(
model_name='page',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_page', to='db.project'),
),
migrations.AddField(
model_name='page',
name='updated_by',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='page_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
),
migrations.AddField(
model_name='page',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_page', to='db.workspace'),
),
migrations.CreateModel(
name='PageFavorite',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='pagefavorite_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('page', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='page_favorites', to='db.page')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_pagefavorite', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='pagefavorite_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='page_favorites', to=settings.AUTH_USER_MODEL)),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_pagefavorite', to='db.workspace')),
],
options={
'verbose_name': 'Page Favorite',
'verbose_name_plural': 'Page Favorites',
'db_table': 'page_favorites',
'ordering': ('-created_at',),
'unique_together': {('page', 'user')},
},
),
]
@@ -0,0 +1,61 @@
# Generated by Django 3.2.18 on 2023-03-30 20:33
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('db', '0024_auto_20230322_0138'),
]
operations = [
migrations.AddField(
model_name='page',
name='color',
field=models.CharField(blank=True, max_length=255),
),
migrations.AddField(
model_name='pageblock',
name='sort_order',
field=models.FloatField(default=65535),
),
migrations.AddField(
model_name='pageblock',
name='sync',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='project',
name='page_view',
field=models.BooleanField(default=True),
),
migrations.CreateModel(
name='PageLabel',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='pagelabel_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('label', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='page_labels', to='db.label')),
('page', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='page_labels', to='db.page')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_pagelabel', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='pagelabel_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_pagelabel', to='db.workspace')),
],
options={
'verbose_name': 'Page Label',
'verbose_name_plural': 'Page Labels',
'db_table': 'page_labels',
'ordering': ('-created_at',),
},
),
migrations.AddField(
model_name='page',
name='labels',
field=models.ManyToManyField(blank=True, related_name='pages', through='db.PageLabel', to='db.Label'),
),
]
@@ -0,0 +1,19 @@
# Generated by Django 3.2.18 on 2023-04-04 21:50
from django.db import migrations, models
import plane.db.models.project
class Migration(migrations.Migration):
dependencies = [
('db', '0025_auto_20230331_0203'),
]
operations = [
migrations.AlterField(
model_name='projectmember',
name='view_props',
field=models.JSONField(default=plane.db.models.project.get_default_props),
),
]
@@ -0,0 +1,97 @@
# Generated by Django 3.2.18 on 2023-04-08 21:42
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import plane.db.models.issue
import uuid
class Migration(migrations.Migration):
dependencies = [
('db', '0026_alter_projectmember_view_props'),
]
operations = [
migrations.CreateModel(
name='Estimate',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('name', models.CharField(max_length=255)),
('description', models.TextField(blank=True, verbose_name='Estimate Description')),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='estimate_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_estimate', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='estimate_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_estimate', to='db.workspace')),
],
options={
'verbose_name': 'Estimate',
'verbose_name_plural': 'Estimates',
'db_table': 'estimates',
'ordering': ('name',),
'unique_together': {('name', 'project')},
},
),
migrations.RemoveField(
model_name='issue',
name='attachments',
),
migrations.AddField(
model_name='issue',
name='estimate_point',
field=models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(7)]),
),
migrations.CreateModel(
name='IssueAttachment',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('attributes', models.JSONField(default=dict)),
('asset', models.FileField(upload_to=plane.db.models.issue.get_upload_path, validators=[plane.db.models.issue.file_size])),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issueattachment_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_attachment', to='db.issue')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_issueattachment', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issueattachment_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_issueattachment', to='db.workspace')),
],
options={
'verbose_name': 'Issue Attachment',
'verbose_name_plural': 'Issue Attachments',
'db_table': 'issue_attachments',
'ordering': ('-created_at',),
},
),
migrations.AddField(
model_name='project',
name='estimate',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='projects', to='db.estimate'),
),
migrations.CreateModel(
name='EstimatePoint',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('key', models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(7)])),
('description', models.TextField(blank=True)),
('value', models.CharField(max_length=20)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='estimatepoint_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('estimate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='points', to='db.estimate')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_estimatepoint', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='estimatepoint_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_estimatepoint', to='db.workspace')),
],
options={
'verbose_name': 'Estimate Point',
'verbose_name_plural': 'Estimate Points',
'db_table': 'estimate_points',
'ordering': ('value',),
'unique_together': {('value', 'estimate')},
},
),
]
@@ -0,0 +1,48 @@
# Generated by Django 3.2.18 on 2023-04-14 11:33
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('db', '0027_auto_20230409_0312'),
]
operations = [
migrations.AddField(
model_name='user',
name='theme',
field=models.JSONField(default=dict),
),
migrations.AlterField(
model_name='issue',
name='estimate_point',
field=models.IntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(7)]),
),
migrations.CreateModel(
name='WorkspaceTheme',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('name', models.CharField(max_length=300)),
('colors', models.JSONField(default=dict)),
('actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='themes', to=settings.AUTH_USER_MODEL)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workspacetheme_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workspacetheme_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='themes', to='db.workspace')),
],
options={
'verbose_name': 'Workspace Theme',
'verbose_name_plural': 'Workspace Themes',
'db_table': 'workspace_themes',
'ordering': ('-created_at',),
'unique_together': {('workspace', 'name')},
},
),
]
@@ -0,0 +1,58 @@
# Generated by Django 3.2.18 on 2023-05-01 19:56
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('db', '0028_auto_20230414_1703'),
]
operations = [
migrations.AddField(
model_name='cycle',
name='view_props',
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name='importer',
name='imported_data',
field=models.JSONField(null=True),
),
migrations.AddField(
model_name='module',
name='view_props',
field=models.JSONField(default=dict),
),
migrations.CreateModel(
name='SlackProjectSync',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('access_token', models.CharField(max_length=300)),
('scopes', models.TextField()),
('bot_user_id', models.CharField(max_length=50)),
('webhook_url', models.URLField(max_length=1000)),
('data', models.JSONField(default=dict)),
('team_id', models.CharField(max_length=30)),
('team_name', models.CharField(max_length=300)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='slackprojectsync_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_slackprojectsync', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='slackprojectsync_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_slackprojectsync', to='db.workspace')),
('workspace_integration', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='slack_syncs', to='db.workspaceintegration')),
],
options={
'verbose_name': 'Slack Project Sync',
'verbose_name_plural': 'Slack Project Syncs',
'db_table': 'slack_project_syncs',
'ordering': ('-created_at',),
'unique_together': {('team_id', 'project')},
},
),
]
@@ -0,0 +1,17 @@
# Generated by Django 3.2.18 on 2023-05-05 14:17
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('db', '0029_auto_20230502_0126'),
]
operations = [
migrations.AlterUniqueTogether(
name='estimatepoint',
unique_together=set(),
),
]
@@ -0,0 +1,37 @@
# Generated by Django 3.2.18 on 2023-05-12 11:31
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('db', '0030_alter_estimatepoint_unique_together'),
]
operations = [
migrations.CreateModel(
name='AnalyticView',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('name', models.CharField(max_length=255)),
('description', models.TextField(blank=True)),
('query', models.JSONField()),
('query_dict', models.JSONField(default=dict)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='analyticview_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='analyticview_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='analytics', to='db.workspace')),
],
options={
'verbose_name': 'Analytic',
'verbose_name_plural': 'Analytics',
'db_table': 'analytic_views',
'ordering': ('-created_at',),
},
),
]
@@ -0,0 +1,23 @@
# Generated by Django 3.2.19 on 2023-05-20 14:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('db', '0031_analyticview'),
]
operations = [
migrations.RenameField(
model_name='project',
old_name='icon',
new_name='emoji',
),
migrations.AddField(
model_name='project',
name='icon_prop',
field=models.JSONField(null=True),
),
]
+13 -1
View File
@@ -8,6 +8,7 @@ from .workspace import (
Team,
WorkspaceMemberInvite,
TeamMember,
WorkspaceTheme,
)
from .project import (
@@ -31,6 +32,8 @@ from .issue import (
Label,
IssueBlocker,
IssueLink,
IssueSequence,
IssueAttachment,
)
from .asset import FileAsset
@@ -43,7 +46,7 @@ from .cycle import Cycle, CycleIssue, CycleFavorite
from .shortcut import Shortcut
from .view import View
from .view import IssueView, IssueViewFavorite
from .module import Module, ModuleMember, ModuleIssue, ModuleLink, ModuleFavorite
@@ -56,4 +59,13 @@ from .integration import (
GithubRepositorySync,
GithubIssueSync,
GithubCommentSync,
SlackProjectSync,
)
from .importer import Importer
from .page import Page, PageBlock, PageFavorite, PageLabel
from .estimate import Estimate, EstimatePoint
from .analytic import AnalyticView
+25
View File
@@ -0,0 +1,25 @@
# Django models
from django.db import models
from django.conf import settings
from .base import BaseModel
class AnalyticView(BaseModel):
workspace = models.ForeignKey(
"db.Workspace", related_name="analytics", on_delete=models.CASCADE
)
name = models.CharField(max_length=255)
description = models.TextField(blank=True)
query = models.JSONField()
query_dict = models.JSONField(default=dict)
class Meta:
verbose_name = "Analytic"
verbose_name_plural = "Analytics"
db_table = "analytic_views"
ordering = ("-created_at",)
def __str__(self):
"""Return name of the analytic view"""
return f"{self.name} <{self.workspace.name}>"
+7 -1
View File
@@ -1,3 +1,6 @@
# Python imports
from uuid import uuid4
# Django import
from django.db import models
from django.core.exceptions import ValidationError
@@ -7,7 +10,9 @@ from . import BaseModel
def get_upload_path(instance, filename):
return f"{instance.workspace.id}/{filename}"
if instance.workspace_id is not None:
return f"{instance.workspace.id}/{uuid4().hex}-{filename}"
return f"user-{uuid4().hex}-{filename}"
def file_size(value):
@@ -15,6 +20,7 @@ def file_size(value):
if value.size > limit:
raise ValidationError("File too large. Size should not exceed 5 MB.")
class FileAsset(BaseModel):
"""
A file asset.
+1
View File
@@ -16,6 +16,7 @@ class Cycle(ProjectBaseModel):
on_delete=models.CASCADE,
related_name="owned_by_cycle",
)
view_props = models.JSONField(default=dict)
class Meta:
verbose_name = "Cycle"
+45
View File
@@ -0,0 +1,45 @@
# Django imports
from django.db import models
from django.core.validators import MinValueValidator, MaxValueValidator
# Module imports
from . import ProjectBaseModel
class Estimate(ProjectBaseModel):
name = models.CharField(max_length=255)
description = models.TextField(verbose_name="Estimate Description", blank=True)
def __str__(self):
"""Return name of the estimate"""
return f"{self.name} <{self.project.name}>"
class Meta:
unique_together = ["name", "project"]
verbose_name = "Estimate"
verbose_name_plural = "Estimates"
db_table = "estimates"
ordering = ("name",)
class EstimatePoint(ProjectBaseModel):
estimate = models.ForeignKey(
"db.Estimate",
on_delete=models.CASCADE,
related_name="points",
)
key = models.IntegerField(
default=0, validators=[MinValueValidator(0), MaxValueValidator(7)]
)
description = models.TextField(blank=True)
value = models.CharField(max_length=20)
def __str__(self):
"""Return name of the estimate"""
return f"{self.estimate.name} <{self.key}> <{self.value}>"
class Meta:
verbose_name = "Estimate Point"
verbose_name_plural = "Estimate Points"
db_table = "estimate_points"
ordering = ("value",)
+46
View File
@@ -0,0 +1,46 @@
# Django imports
from django.db import models
from django.conf import settings
# Module imports
from . import ProjectBaseModel
class Importer(ProjectBaseModel):
service = models.CharField(
max_length=50,
choices=(
("github", "GitHub"),
("jira", "Jira"),
),
)
status = models.CharField(
max_length=50,
choices=(
("queued", "Queued"),
("processing", "Processing"),
("completed", "Completed"),
("failed", "Failed"),
),
default="queued",
)
initiated_by = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="imports"
)
metadata = models.JSONField(default=dict)
config = models.JSONField(default=dict)
data = models.JSONField(default=dict)
token = models.ForeignKey(
"db.APIToken", on_delete=models.CASCADE, related_name="importer"
)
imported_data = models.JSONField(null=True)
class Meta:
verbose_name = "Importer"
verbose_name_plural = "Importers"
db_table = "importers"
ordering = ("-created_at",)
def __str__(self):
"""Return name of the service"""
return f"{self.service} <{self.project.name}>"
@@ -1,2 +1,3 @@
from .base import Integration, WorkspaceIntegration
from .github import GithubRepository, GithubRepositorySync, GithubIssueSync, GithubCommentSync
from .slack import SlackProjectSync
@@ -0,0 +1,32 @@
# Python imports
import uuid
# Django imports
from django.db import models
# Module imports
from plane.db.models import ProjectBaseModel
class SlackProjectSync(ProjectBaseModel):
access_token = models.CharField(max_length=300)
scopes = models.TextField()
bot_user_id = models.CharField(max_length=50)
webhook_url = models.URLField(max_length=1000)
data = models.JSONField(default=dict)
team_id = models.CharField(max_length=30)
team_name = models.CharField(max_length=300)
workspace_integration = models.ForeignKey(
"db.WorkspaceIntegration", related_name="slack_syncs", on_delete=models.CASCADE
)
def __str__(self):
"""Return the repo name"""
return f"{self.project.name}"
class Meta:
unique_together = ["team_id", "project"]
verbose_name = "Slack Project Sync"
verbose_name_plural = "Slack Project Syncs"
db_table = "slack_project_syncs"
ordering = ("-created_at",)
+61 -9
View File
@@ -1,3 +1,6 @@
# Python import
from uuid import uuid4
# Django imports
from django.contrib.postgres.fields import ArrayField
from django.db import models
@@ -5,6 +8,8 @@ from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils import timezone
from django.core.validators import MinValueValidator, MaxValueValidator
from django.core.exceptions import ValidationError
# Module imports
from . import ProjectBaseModel
@@ -33,6 +38,9 @@ class Issue(ProjectBaseModel):
blank=True,
related_name="state_issue",
)
estimate_point = models.IntegerField(
validators=[MinValueValidator(0), MaxValueValidator(7)], null=True, blank=True
)
name = models.CharField(max_length=255, verbose_name="Issue Name")
description = models.JSONField(blank=True, default=dict)
description_html = models.TextField(blank=True, default="<p></p>")
@@ -54,7 +62,6 @@ class Issue(ProjectBaseModel):
through_fields=("issue", "assignee"),
)
sequence_id = models.IntegerField(default=1, verbose_name="Issue Sequence ID")
attachments = ArrayField(models.URLField(), size=10, blank=True, default=list)
labels = models.ManyToManyField(
"db.Label", blank=True, related_name="labels", through="IssueLabel"
)
@@ -78,30 +85,39 @@ class Issue(ProjectBaseModel):
).first()
# if there is no default state assign any random state
if default_state is None:
self.state = State.objects.filter(project=self.project).first()
random_state = State.objects.filter(project=self.project).first()
self.state = random_state
if random_state.group == "started":
self.start_date = timezone.now().date()
else:
if default_state.group == "started":
self.start_date = timezone.now().date()
self.state = default_state
except ImportError:
pass
else:
try:
from plane.db.models import State
from plane.db.models import State, PageBlock
# Get the completed states of the project
completed_states = State.objects.filter(
group="completed", project=self.project
).values_list("pk", flat=True)
# Check if the current issue state and completed state id are same
if self.state.id in completed_states:
if self.state.group == "completed":
self.completed_at = timezone.now()
# check if there are any page blocks
PageBlock.objects.filter(issue_id=self.id).filter().update(
completed_at=timezone.now()
)
elif self.state.group == "started":
self.start_date = timezone.now().date()
else:
PageBlock.objects.filter(issue_id=self.id).filter().update(
completed_at=None
)
self.completed_at = None
except ImportError:
pass
if self._state.adding:
# Get the maximum display_id value from the database
last_id = IssueSequence.objects.filter(project=self.project).aggregate(
largest=models.Max("sequence")
)["largest"]
@@ -116,6 +132,9 @@ class Issue(ProjectBaseModel):
if largest_sort_order is not None:
self.sort_order = largest_sort_order + 10000
# If adding it to started state
if self.state.group == "started":
self.start_date = timezone.now().date()
# Strip the html tags using html parser
self.description_stripped = (
None
@@ -186,6 +205,38 @@ class IssueLink(ProjectBaseModel):
return f"{self.issue.name} {self.url}"
def get_upload_path(instance, filename):
return f"{instance.workspace.id}/{uuid4().hex}-{filename}"
def file_size(value):
limit = 5 * 1024 * 1024
if value.size > limit:
raise ValidationError("File too large. Size should not exceed 5 MB.")
class IssueAttachment(ProjectBaseModel):
attributes = models.JSONField(default=dict)
asset = models.FileField(
upload_to=get_upload_path,
validators=[
file_size,
],
)
issue = models.ForeignKey(
"db.Issue", on_delete=models.CASCADE, related_name="issue_attachment"
)
class Meta:
verbose_name = "Issue Attachment"
verbose_name_plural = "Issue Attachments"
db_table = "issue_attachments"
ordering = ("-created_at",)
def __str__(self):
return f"{self.issue.name} {self.asset}"
class IssueActivity(ProjectBaseModel):
issue = models.ForeignKey(
Issue, on_delete=models.SET_NULL, null=True, related_name="issue_activity"
@@ -307,6 +358,7 @@ class Label(ProjectBaseModel):
color = models.CharField(max_length=255, blank=True)
class Meta:
unique_together = ["name", "project"]
verbose_name = "Label"
verbose_name_plural = "Labels"
db_table = "labels"
+1
View File
@@ -39,6 +39,7 @@ class Module(ProjectBaseModel):
through="ModuleMember",
through_fields=("module", "member"),
)
view_props = models.JSONField(default=dict)
class Meta:
unique_together = ["name", "project"]
+126
View File
@@ -0,0 +1,126 @@
# Django imports
from django.db import models
from django.conf import settings
# Module imports
from . import ProjectBaseModel
from plane.utils.html_processor import strip_tags
class Page(ProjectBaseModel):
name = models.CharField(max_length=255)
description = models.JSONField(default=dict, blank=True)
description_html = models.TextField(blank=True, default="<p></p>")
description_stripped = models.TextField(blank=True, null=True)
owned_by = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="pages"
)
access = models.PositiveSmallIntegerField(
choices=((0, "Public"), (1, "Private")), default=0
)
color = models.CharField(max_length=255, blank=True)
labels = models.ManyToManyField(
"db.Label", blank=True, related_name="pages", through="db.PageLabel"
)
class Meta:
verbose_name = "Page"
verbose_name_plural = "Pages"
db_table = "pages"
ordering = ("-created_at",)
def __str__(self):
"""Return owner email and page name"""
return f"{self.owned_by.email} <{self.name}>"
class PageBlock(ProjectBaseModel):
page = models.ForeignKey("db.Page", on_delete=models.CASCADE, related_name="blocks")
name = models.CharField(max_length=255)
description = models.JSONField(default=dict, blank=True)
description_html = models.TextField(blank=True, default="<p></p>")
description_stripped = models.TextField(blank=True, null=True)
issue = models.ForeignKey(
"db.Issue", on_delete=models.SET_NULL, related_name="blocks", null=True
)
completed_at = models.DateTimeField(null=True)
sort_order = models.FloatField(default=65535)
sync = models.BooleanField(default=True)
def save(self, *args, **kwargs):
if self._state.adding:
largest_sort_order = PageBlock.objects.filter(
project=self.project, page=self.page
).aggregate(largest=models.Max("sort_order"))["largest"]
if largest_sort_order is not None:
self.sort_order = largest_sort_order + 10000
# Strip the html tags using html parser
self.description_stripped = (
None
if (self.description_html == "" or self.description_html is None)
else strip_tags(self.description_html)
)
if self.completed_at and self.issue:
try:
from plane.db.models import State, Issue
completed_state = State.objects.filter(
group="completed", project=self.project
).first()
if completed_state is not None:
Issue.objects.update(pk=self.issue_id, state=completed_state)
except ImportError:
pass
super(PageBlock, self).save(*args, **kwargs)
class Meta:
verbose_name = "Page Block"
verbose_name_plural = "Page Blocks"
db_table = "page_blocks"
ordering = ("-created_at",)
def __str__(self):
"""Return page and page block"""
return f"{self.page.name} <{self.name}>"
class PageFavorite(ProjectBaseModel):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="page_favorites",
)
page = models.ForeignKey(
"db.Page", on_delete=models.CASCADE, related_name="page_favorites"
)
class Meta:
unique_together = ["page", "user"]
verbose_name = "Page Favorite"
verbose_name_plural = "Page Favorites"
db_table = "page_favorites"
ordering = ("-created_at",)
def __str__(self):
"""Return user and the page"""
return f"{self.user.email} <{self.page.name}>"
class PageLabel(ProjectBaseModel):
label = models.ForeignKey(
"db.Label", on_delete=models.CASCADE, related_name="page_labels"
)
page = models.ForeignKey(
"db.Page", on_delete=models.CASCADE, related_name="page_labels"
)
class Meta:
verbose_name = "Page Label"
verbose_name_plural = "Page Labels"
db_table = "page_labels"
ordering = ("-created_at",)
def __str__(self):
return f"{self.page.name} {self.label.name}"
+14 -5
View File
@@ -21,10 +21,13 @@ ROLE_CHOICES = (
def get_default_props():
return {
"filters": {"type": None},
"orderBy": "-created_at",
"collapsed": True,
"issueView": "list",
"groupByProperty": None,
"orderBy": None,
"filterIssue": None,
"groupByProperty": None,
"showEmptyGroups": True,
}
@@ -60,10 +63,16 @@ class Project(BaseModel):
null=True,
blank=True,
)
icon = models.CharField(max_length=255, null=True, blank=True)
emoji = models.CharField(max_length=255, null=True, blank=True)
icon_prop = models.JSONField(null=True)
module_view = models.BooleanField(default=True)
cycle_view = models.BooleanField(default=True)
cover_image = models.URLField(blank=True, null=True)
issue_views_view = models.BooleanField(default=True)
page_view = models.BooleanField(default=True)
cover_image = models.URLField(blank=True, null=True, max_length=800)
estimate = models.ForeignKey(
"db.Estimate", on_delete=models.SET_NULL, related_name="projects", null=True
)
def __str__(self):
"""Return name of the project"""
@@ -125,7 +134,7 @@ class ProjectMember(ProjectBaseModel):
)
comment = models.TextField(blank=True, null=True)
role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, default=10)
view_props = models.JSONField(null=True)
view_props = models.JSONField(default=get_default_props)
default_props = models.JSONField(default=get_default_props)
class Meta:
+15 -21
View File
@@ -11,9 +11,12 @@ from django.utils import timezone
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
# Third party imports
from sentry_sdk import capture_exception
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
class User(AbstractBaseUser, PermissionsMixin):
@@ -69,6 +72,7 @@ class User(AbstractBaseUser, PermissionsMixin):
my_issues_prop = models.JSONField(null=True)
role = models.CharField(max_length=300, null=True, blank=True)
is_bot = models.BooleanField(default=False)
theme = models.JSONField(default=dict)
USERNAME_FIELD = "email"
@@ -100,29 +104,19 @@ class User(AbstractBaseUser, PermissionsMixin):
@receiver(post_save, sender=User)
def send_welcome_email(sender, instance, created, **kwargs):
def send_welcome_slack(sender, instance, created, **kwargs):
try:
if created and not instance.is_bot:
first_name = instance.first_name.capitalize()
to_email = instance.email
from_email_string = f"Team Plane <team@mailer.plane.so>"
subject = f"Welcome to Plane ✈️!"
context = {"first_name": first_name, "email": instance.email}
html_content = render_to_string(
"emails/auth/user_welcome_email.html", context
)
text_content = strip_tags(html_content)
msg = EmailMultiAlternatives(
subject, text_content, from_email_string, [to_email]
)
msg.attach_alternative(html_content, "text/html")
msg.send()
# Send message on slack as well
if settings.SLACK_BOT_TOKEN:
client = WebClient(token=settings.SLACK_BOT_TOKEN)
try:
_ = client.chat_postMessage(
channel="#trackers",
text=f"New user {instance.email} has signed up and begun the onboarding journey.",
)
except SlackApiError as e:
print(f"Got an error: {e.response['error']}")
return
except Exception as e:
capture_exception(e)
+31 -5
View File
@@ -1,22 +1,48 @@
# Django imports
from django.db import models
from django.conf import settings
# Module import
from . import ProjectBaseModel
class View(ProjectBaseModel):
class IssueView(ProjectBaseModel):
name = models.CharField(max_length=255, verbose_name="View Name")
description = models.TextField(verbose_name="View Description", blank=True)
query = models.JSONField(verbose_name="View Query")
access = models.PositiveSmallIntegerField(
default=1, choices=((0, "Private"), (1, "Public"))
)
query_data = models.JSONField(default=dict)
class Meta:
verbose_name = "View"
verbose_name_plural = "Views"
db_table = "views"
verbose_name = "Issue View"
verbose_name_plural = "Issue Views"
db_table = "issue_views"
ordering = ("-created_at",)
def __str__(self):
"""Return name of the View"""
return f"{self.name} <{self.project.name}>"
class IssueViewFavorite(ProjectBaseModel):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="user_view_favorites",
)
view = models.ForeignKey(
"db.IssueView", on_delete=models.CASCADE, related_name="view_favorites"
)
class Meta:
unique_together = ["view", "user"]
verbose_name = "View Favorite"
verbose_name_plural = "View Favorites"
db_table = "view_favorites"
ordering = ("-created_at",)
def __str__(self):
"""Return user and the view"""
return f"{self.user.email} <{self.view.name}>"
+21 -2
View File
@@ -36,7 +36,6 @@ class Workspace(BaseModel):
ordering = ("-created_at",)
class WorkspaceMember(BaseModel):
workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="workspace_member"
@@ -111,7 +110,6 @@ class Team(BaseModel):
class TeamMember(BaseModel):
workspace = models.ForeignKey(
Workspace, on_delete=models.CASCADE, related_name="team_member"
)
@@ -129,3 +127,24 @@ class TeamMember(BaseModel):
verbose_name_plural = "Team Members"
db_table = "team_members"
ordering = ("-created_at",)
class WorkspaceTheme(BaseModel):
workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="themes"
)
name = models.CharField(max_length=300)
actor = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="themes"
)
colors = models.JSONField(default=dict)
def __str__(self):
return str(self.name) + str(self.actor.email)
class Meta:
unique_together = ["workspace", "name"]
verbose_name = "Workspace Theme"
verbose_name_plural = "Workspace Themes"
db_table = "workspace_themes"
ordering = ("-created_at",)
+8 -3
View File
@@ -35,7 +35,6 @@ INSTALLED_APPS = [
"rest_framework_simplejwt.token_blacklist",
"corsheaders",
"taggit",
"django_rq",
]
MIDDLEWARE = [
@@ -48,6 +47,7 @@ MIDDLEWARE = [
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"crum.CurrentRequestUserMiddleware",
"django.middleware.gzip.GZipMiddleware",
]
REST_FRAMEWORK = {
@@ -174,11 +174,12 @@ EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
# Host for sending e-mail.
EMAIL_HOST = os.environ.get("EMAIL_HOST")
# Port for sending e-mail.
EMAIL_PORT = 587
EMAIL_PORT = int(os.environ.get("EMAIL_PORT", 587))
# Optional SMTP authentication information for EMAIL_HOST.
EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER")
EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD")
EMAIL_USE_TLS = True
EMAIL_USE_TLS = os.environ.get("EMAIL_USE_TLS", "1") == "1"
EMAIL_FROM = os.environ.get("EMAIL_FROM", "Team Plane <team@mailer.plane.so>")
SIMPLE_JWT = {
@@ -207,3 +208,7 @@ SIMPLE_JWT = {
"SLIDING_TOKEN_LIFETIME": timedelta(minutes=5),
"SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1),
}
CELERY_TIMEZONE = TIME_ZONE
CELERY_TASK_SERIALIZER = 'json'
CELERY_ACCEPT_CONTENT = ['application/json']
+17 -9
View File
@@ -59,16 +59,8 @@ if os.environ.get("SENTRY_DSN", False):
REDIS_HOST = "localhost"
REDIS_PORT = 6379
REDIS_URL = False
REDIS_URL = os.environ.get("REDIS_URL")
RQ_QUEUES = {
"default": {
"HOST": "localhost",
"PORT": 6379,
"DB": 0,
"DEFAULT_TIMEOUT": 360,
},
}
MEDIA_URL = "/uploads/"
MEDIA_ROOT = os.path.join(BASE_DIR, "uploads")
@@ -78,3 +70,19 @@ if DOCKERIZED:
WEB_URL = os.environ.get("WEB_URL", "localhost:3000")
PROXY_BASE_URL = os.environ.get("PROXY_BASE_URL", False)
ANALYTICS_SECRET_KEY = os.environ.get("ANALYTICS_SECRET_KEY", False)
ANALYTICS_BASE_API = os.environ.get("ANALYTICS_BASE_API", False)
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False)
GPT_ENGINE = os.environ.get("GPT_ENGINE", "text-davinci-003")
SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False)
LOGGER_BASE_URL = os.environ.get("LOGGER_BASE_URL", False)
CELERY_RESULT_BACKEND = os.environ.get("REDIS_URL")
CELERY_BROKER_URL = os.environ.get("REDIS_URL")
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
+51 -10
View File
@@ -1,5 +1,7 @@
"""Production settings and globals."""
from urllib.parse import urlparse
import ssl
import certifi
import dj_database_url
from urllib.parse import urlparse
@@ -22,13 +24,7 @@ DATABASES = {
}
}
# CORS WHITELIST ON PROD
CORS_ORIGIN_WHITELIST = [
# "https://example.com",
# "https://sub.example.com",
# "http://localhost:8080",
# "http://127.0.0.1:9000"
]
# Parse database configuration from $DATABASE_URL
DATABASES["default"] = dj_database_url.config()
SITE_ID = 1
@@ -43,12 +39,33 @@ DOCKERIZED = os.environ.get(
# Honor the 'X-Forwarded-Proto' header for request.is_secure()
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
# Allow all host headers
ALLOWED_HOSTS = ["*"]
# TODO: Make it FALSE and LIST DOMAINS IN FULL PROD.
CORS_ALLOW_ALL_ORIGINS = True
CORS_ALLOW_METHODS = [
"DELETE",
"GET",
"OPTIONS",
"PATCH",
"POST",
"PUT",
]
CORS_ALLOW_HEADERS = [
"accept",
"accept-encoding",
"authorization",
"content-type",
"dnt",
"origin",
"user-agent",
"x-csrftoken",
"x-requested-with",
]
CORS_ALLOW_CREDENTIALS = True
# Simplified static file serving.
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
@@ -88,7 +105,7 @@ if (
AWS_S3_ADDRESSING_STYLE = "auto"
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
AWS_S3_ENDPOINT_URL = ""
AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "")
# A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator.
AWS_S3_KEY_PREFIX = ""
@@ -211,3 +228,27 @@ RQ_QUEUES = {
WEB_URL = os.environ.get("WEB_URL")
PROXY_BASE_URL = os.environ.get("PROXY_BASE_URL", False)
ANALYTICS_SECRET_KEY = os.environ.get("ANALYTICS_SECRET_KEY", False)
ANALYTICS_BASE_API = os.environ.get("ANALYTICS_BASE_API", False)
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False)
GPT_ENGINE = os.environ.get("GPT_ENGINE", "text-davinci-003")
SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False)
LOGGER_BASE_URL = os.environ.get("LOGGER_BASE_URL", False)
redis_url = os.environ.get("REDIS_URL")
broker_url = (
f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
)
if DOCKERIZED:
CELERY_BROKER_URL = REDIS_URL
CELERY_RESULT_BACKEND = REDIS_URL
else:
CELERY_RESULT_BACKEND = broker_url
CELERY_BROKER_URL = broker_url
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
+19 -17
View File
@@ -1,23 +1,25 @@
import os
import redis
from django.conf import settings
from urllib.parse import urlparse
def redis_instance():
# Run in local redis url is false
if not settings.REDIS_URL:
ri = redis.StrictRedis(host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=0)
# connect to redis
if (
settings.DOCKERIZED
or os.environ.get("DJANGO_SETTINGS_MODULE", "plane.settings.production")
== "plane.settings.local"
):
ri = redis.Redis.from_url(settings.REDIS_URL, db=0)
else:
# Run in prod redis url is true check with dockerized value
if settings.DOCKERIZED:
ri = redis.from_url(settings.REDIS_URL, db=0)
else:
url = urlparse(settings.REDIS_URL)
ri = redis.Redis(
host=url.hostname,
port=url.port,
password=url.password,
ssl=True,
ssl_cert_reqs=None,
)
return ri
url = urlparse(settings.REDIS_URL)
ri = redis.Redis(
host=url.hostname,
port=url.port,
password=url.password,
ssl=True,
ssl_cert_reqs=None,
)
return ri
+21 -2
View File
@@ -1,5 +1,7 @@
"""Production settings and globals."""
from urllib.parse import urlparse
import ssl
import certifi
import dj_database_url
from urllib.parse import urlparse
@@ -9,7 +11,6 @@ from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.redis import RedisIntegration
from .common import * # noqa
# Database
DEBUG = True
DATABASES = {
@@ -79,7 +80,7 @@ AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME")
AWS_S3_ADDRESSING_STYLE = "auto"
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
AWS_S3_ENDPOINT_URL = ""
AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "")
# A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator.
AWS_S3_KEY_PREFIX = ""
@@ -187,3 +188,21 @@ RQ_QUEUES = {
WEB_URL = os.environ.get("WEB_URL")
PROXY_BASE_URL = os.environ.get("PROXY_BASE_URL", False)
ANALYTICS_SECRET_KEY = os.environ.get("ANALYTICS_SECRET_KEY", False)
ANALYTICS_BASE_API = os.environ.get("ANALYTICS_BASE_API", False)
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False)
GPT_ENGINE = os.environ.get("GPT_ENGINE", "text-davinci-003")
SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False)
LOGGER_BASE_URL = os.environ.get("LOGGER_BASE_URL", False)
redis_url = os.environ.get("REDIS_URL")
broker_url = f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
CELERY_RESULT_BACKEND = broker_url
CELERY_BROKER_URL = broker_url
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
+76
View File
@@ -0,0 +1,76 @@
# Python imports
from itertools import groupby
# Django import
from django.db import models
from django.db.models import Count, F, Sum, Value, Case, When, CharField
from django.db.models.functions import Coalesce, ExtractMonth, ExtractYear, Concat
def build_graph_plot(queryset, x_axis, y_axis, segment=None):
temp_axis = x_axis
if x_axis in ["created_at", "start_date", "target_date", "completed_at"]:
year = ExtractYear(x_axis)
month = ExtractMonth(x_axis)
dimension = Concat(year, Value("-"), month, output_field=CharField())
queryset = queryset.annotate(dimension=dimension)
x_axis = "dimension"
else:
queryset = queryset.annotate(dimension=F(x_axis))
x_axis = "dimension"
if x_axis in ["created_at", "start_date", "target_date", "completed_at"]:
queryset = queryset.exclude(x_axis__is_null=True)
if segment in ["created_at", "start_date", "target_date", "completed_at"]:
year = ExtractYear(segment)
month = ExtractMonth(segment)
dimension = Concat(year, Value("-"), month, output_field=CharField())
queryset = queryset.annotate(segmented=dimension)
segment = "segmented"
queryset = queryset.values(x_axis)
# Group queryset by x_axis field
if y_axis == "issue_count":
queryset = queryset.annotate(
is_null=Case(
When(dimension__isnull=True, then=Value("None")),
default=Value("not_null"),
output_field=models.CharField(max_length=8),
),
dimension_ex=Coalesce("dimension", Value("null")),
).values("dimension")
if segment:
queryset = queryset.annotate(segment=F(segment)).values(
"dimension", "segment"
)
else:
queryset = queryset.values("dimension")
queryset = queryset.annotate(count=Count("*")).order_by("dimension")
if y_axis == "estimate":
queryset = queryset.annotate(estimate=Sum("estimate_point")).order_by(x_axis)
if segment:
queryset = queryset.annotate(segment=F(segment)).values(
"dimension", "segment", "estimate"
)
else:
queryset = queryset.values("dimension", "estimate")
result_values = list(queryset)
grouped_data = {}
for key, items in groupby(result_values, key=lambda x: x[str("dimension")]):
grouped_data[str(key)] = list(items)
sorted_data = grouped_data
if temp_axis == "priority":
order = ["low", "medium", "high", "urgent", "None"]
sorted_data = {key: grouped_data[key] for key in order if key in grouped_data}
else:
sorted_data = dict(sorted(grouped_data.items(), key=lambda x: (x[0] == "None", x[0])))
return sorted_data
+9
View File
@@ -27,6 +27,15 @@ def group_results(results_data, group_by):
"""
response_dict = dict()
if group_by == "priority":
response_dict = {
"urgent": [],
"high": [],
"medium": [],
"low": [],
"None": [],
}
for value in results_data:
group_attribute = resolve_keys(group_by, value)
if isinstance(group_attribute, list):
+53
View File
@@ -0,0 +1,53 @@
import requests
from requests.auth import HTTPBasicAuth
from sentry_sdk import capture_exception
def jira_project_issue_summary(email, api_token, project_key, hostname):
try:
auth = HTTPBasicAuth(email, api_token)
headers = {"Accept": "application/json"}
issue_url = f"https://{hostname}/rest/api/3/search?jql=project={project_key} AND issuetype=Story"
issue_response = requests.request(
"GET", issue_url, headers=headers, auth=auth
).json()["total"]
module_url = f"https://{hostname}/rest/api/3/search?jql=project={project_key} AND issuetype=Epic"
module_response = requests.request(
"GET", module_url, headers=headers, auth=auth
).json()["total"]
status_url = f"https://{hostname}/rest/api/3/status/?jql=project={project_key}"
status_response = requests.request(
"GET", status_url, headers=headers, auth=auth
).json()
labels_url = f"https://{hostname}/rest/api/3/label/?jql=project={project_key}"
labels_response = requests.request(
"GET", labels_url, headers=headers, auth=auth
).json()["total"]
users_url = (
f"https://{hostname}/rest/api/3/users/search?jql=project={project_key}"
)
users_response = requests.request(
"GET", users_url, headers=headers, auth=auth
).json()
return {
"issues": issue_response,
"modules": module_response,
"labels": labels_response,
"states": len(status_response),
"users": (
[
user
for user in users_response
if user.get("accountType") == "atlassian"
]
),
}
except Exception as e:
capture_exception(e)
return {"error": "Something went wrong could not fetch information from jira"}
+83 -5
View File
@@ -1,9 +1,11 @@
import os
import jwt
import requests
from urllib.parse import urlparse, parse_qs
from datetime import datetime, timedelta
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.hazmat.backends import default_backend
from django.conf import settings
def get_jwt_token():
@@ -30,7 +32,7 @@ def get_github_metadata(installation_id):
url = f"https://api.github.com/app/installations/{installation_id}"
headers = {
"Authorization": "Bearer " + token,
"Authorization": "Bearer " + str(token),
"Accept": "application/vnd.github+json",
}
response = requests.get(url, headers=headers).json()
@@ -41,7 +43,7 @@ def get_github_repos(access_tokens_url, repositories_url):
token = get_jwt_token()
headers = {
"Authorization": "Bearer " + token,
"Authorization": "Bearer " + str(token),
"Accept": "application/vnd.github+json",
}
@@ -50,9 +52,9 @@ def get_github_repos(access_tokens_url, repositories_url):
headers=headers,
).json()
oauth_token = oauth_response.get("token")
oauth_token = oauth_response.get("token", "")
headers = {
"Authorization": "Bearer " + oauth_token,
"Authorization": "Bearer " + str(oauth_token),
"Accept": "application/vnd.github+json",
}
response = requests.get(
@@ -67,8 +69,84 @@ def delete_github_installation(installation_id):
url = f"https://api.github.com/app/installations/{installation_id}"
headers = {
"Authorization": "Bearer " + token,
"Authorization": "Bearer " + str(token),
"Accept": "application/vnd.github+json",
}
response = requests.delete(url, headers=headers)
return response
def get_github_repo_details(access_tokens_url, owner, repo):
token = get_jwt_token()
headers = {
"Authorization": "Bearer " + str(token),
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
}
oauth_response = requests.post(
access_tokens_url,
headers=headers,
).json()
oauth_token = oauth_response.get("token")
headers = {
"Authorization": "Bearer " + oauth_token,
"Accept": "application/vnd.github+json",
}
open_issues = requests.get(
f"https://api.github.com/repos/{owner}/{repo}",
headers=headers,
).json()["open_issues_count"]
total_labels = 0
labels_response = requests.get(
f"https://api.github.com/repos/{owner}/{repo}/labels?per_page=100&page=1",
headers=headers,
)
# Check if there are more pages
if len(labels_response.links.keys()):
# get the query parameter of last
last_url = labels_response.links.get("last").get("url")
parsed_url = urlparse(last_url)
last_page_value = parse_qs(parsed_url.query)["page"][0]
total_labels = total_labels + 100 * (last_page_value - 1)
# Get labels in last page
last_page_labels = requests.get(last_url, headers=headers).json()
total_labels = total_labels + len(last_page_labels)
else:
total_labels = len(labels_response.json())
# Currently only supporting upto 100 collaborators
# TODO: Update this function to fetch all collaborators
collaborators = requests.get(
f"https://api.github.com/repos/{owner}/{repo}/collaborators?per_page=100&page=1",
headers=headers,
).json()
return open_issues, total_labels, collaborators
def get_release_notes():
token = settings.GITHUB_ACCESS_TOKEN
if token:
headers = {
"Authorization": "Bearer " + str(token),
"Accept": "application/vnd.github.v3+json",
}
else:
headers = {
"Accept": "application/vnd.github.v3+json",
}
url = "https://api.github.com/repos/makeplane/plane/releases?per_page=5&page=1"
response = requests.get(url, headers=headers)
if response.status_code != 200:
return {"error": "Unable to render information from Github Repository"}
return response.json()
+262
View File
@@ -0,0 +1,262 @@
from django.utils.timezone import make_aware
from django.utils.dateparse import parse_datetime
def filter_state(params, filter, method):
if method == "GET":
states = params.get("state").split(",")
if len(states) and "" not in states:
filter["state__in"] = states
else:
if params.get("state", None) and len(params.get("state")):
filter["state__in"] = params.get("state")
return filter
def filter_estimate_point(params, filter, method):
if method == "GET":
estimate_points = params.get("estimate_point").split(",")
if len(estimate_points) and "" not in estimate_points:
filter["estimate_point__in"] = estimate_points
else:
if params.get("estimate_point", None) and len(params.get("estimate_point")):
filter["estimate_point__in"] = params.get("estimate_point")
return filter
def filter_priority(params, filter, method):
if method == "GET":
priorties = params.get("priority").split(",")
if len(priorties) and "" not in priorties:
filter["priority__in"] = priorties
else:
if params.get("priority", None) and len(params.get("priority")):
filter["priority__in"] = params.get("priority")
return filter
def filter_parent(params, filter, method):
if method == "GET":
parents = params.get("parent").split(",")
if len(parents) and "" not in parents:
filter["parent__in"] = parents
else:
if params.get("parent", None) and len(params.get("parent")):
filter["parent__in"] = params.get("parent")
return filter
def filter_labels(params, filter, method):
if method == "GET":
labels = params.get("labels").split(",")
if len(labels) and "" not in labels:
filter["labels__in"] = labels
else:
if params.get("labels", None) and len(params.get("labels")):
filter["labels__in"] = params.get("labels")
return filter
def filter_assignees(params, filter, method):
if method == "GET":
assignees = params.get("assignees").split(",")
if len(assignees) and "" not in assignees:
filter["assignees__in"] = assignees
else:
if params.get("assignees", None) and len(params.get("assignees")):
filter["assignees__in"] = params.get("assignees")
return filter
def filter_created_by(params, filter, method):
if method == "GET":
created_bys = params.get("created_by").split(",")
if len(created_bys) and "" not in created_bys:
filter["created_by__in"] = created_bys
else:
if params.get("created_by", None) and len(params.get("created_by")):
filter["created_by__in"] = params.get("created_by")
return filter
def filter_name(params, filter, method):
if params.get("name", "") != "":
filter["name__icontains"] = params.get("name")
return filter
def filter_created_at(params, filter, method):
if method == "GET":
created_ats = params.get("created_at").split(",")
if len(created_ats) and "" not in created_ats:
for query in created_ats:
created_at_query = query.split(";")
if len(created_at_query) == 2 and "after" in created_at_query:
filter["created_at__date__gte"] = created_at_query[0]
else:
filter["created_at__date__lte"] = created_at_query[0]
else:
if params.get("created_at", None) and len(params.get("created_at")):
for query in params.get("created_at"):
if query.get("timeline", "after") == "after":
filter["created_at__date__gte"] = query.get("datetime")
else:
filter["created_at__date__lte"] = query.get("datetime")
return filter
def filter_updated_at(params, filter, method):
if method == "GET":
updated_ats = params.get("updated_at").split(",")
if len(updated_ats) and "" not in updated_ats:
for query in updated_ats:
updated_at_query = query.split(";")
if len(updated_at_query) == 2 and "after" in updated_at_query:
filter["updated_at__date__gte"] = updated_at_query[0]
else:
filter["updated_at__date__lte"] = updated_at_query[0]
else:
if params.get("updated_at", None) and len(params.get("updated_at")):
for query in params.get("updated_at"):
if query.get("timeline", "after") == "after":
filter["updated_at__date__gte"] = query.get("datetime")
else:
filter["updated_at__date__lte"] = query.get("datetime")
return filter
def filter_start_date(params, filter, method):
if method == "GET":
start_dates = params.get("start_date").split(",")
if len(start_dates) and "" not in start_dates:
for query in start_dates:
start_date_query = query.split(";")
if len(start_date_query) == 2 and "after" in start_date_query:
filter["start_date__gte"] = start_date_query[0]
else:
filter["start_date__lte"] = start_date_query[0]
else:
if params.get("start_date", None) and len(params.get("start_date")):
for query in params.get("start_date"):
if query.get("timeline", "after") == "after":
filter["start_date__gte"] = query.get("datetime")
else:
filter["start_date__lte"] = query.get("datetime")
return filter
def filter_target_date(params, filter, method):
if method == "GET":
target_dates = params.get("target_date").split(",")
if len(target_dates) and "" not in target_dates:
for query in target_dates:
target_date_query = query.split(";")
if len(target_date_query) == 2 and "after" in target_date_query:
filter["target_date__gte"] = target_date_query[0]
else:
filter["target_date__lte"] = target_date_query[0]
else:
if params.get("target_date", None) and len(params.get("target_date")):
for query in params.get("target_date"):
if query.get("timeline", "after") == "after":
filter["target_date__gte"] = query.get("datetime")
else:
filter["target_date__lte"] = query.get("datetime")
return filter
def filter_completed_at(params, filter, method):
if method == "GET":
completed_ats = params.get("completed_at").split(",")
if len(completed_ats) and "" not in completed_ats:
for query in completed_ats:
completed_at_query = query.split(";")
if len(completed_at_query) == 2 and "after" in completed_at_query:
filter["completed_at__date__gte"] = completed_at_query[0]
else:
filter["completed_at__lte"] = completed_at_query[0]
else:
if params.get("completed_at", None) and len(params.get("completed_at")):
for query in params.get("completed_at"):
if query.get("timeline", "after") == "after":
filter["completed_at__date__gte"] = query.get("datetime")
else:
filter["completed_at__lte"] = query.get("datetime")
return filter
def filter_issue_state_type(params, filter, method):
type = params.get("type", "all")
group = ["backlog", "unstarted", "started", "completed", "cancelled"]
if type == "backlog":
group = ["backlog"]
if type == "active":
group = ["unstarted", "started"]
filter["state__group__in"] = group
return filter
def filter_project(params, filter, method):
if method == "GET":
projects = params.get("project").split(",")
if len(projects) and "" not in projects:
filter["project__in"] = projects
else:
if params.get("project", None) and len(params.get("project")):
filter["project__in"] = params.get("project")
return filter
def filter_cycle(params, filter, method):
if method == "GET":
cycles = params.get("cycle").split(",")
if len(cycles) and "" not in cycles:
filter["issue_cycle__cycle_id__in"] = cycles
else:
if params.get("cycle", None) and len(params.get("cycle")):
filter["issue_cycle__cycle_id__in"] = params.get("cycle")
return filter
def filter_module(params, filter, method):
if method == "GET":
modules = params.get("module").split(",")
if len(modules) and "" not in modules:
filter["issue_module__module_id__in"] = modules
else:
if params.get("module", None) and len(params.get("module")):
filter["issue_module__module_id__in"] = params.get("module")
return filter
def issue_filters(query_params, method):
filter = dict()
ISSUE_FILTER = {
"state": filter_state,
"estimate_point": filter_estimate_point,
"priority": filter_priority,
"parent": filter_parent,
"labels": filter_labels,
"assignees": filter_assignees,
"created_by": filter_created_by,
"name": filter_name,
"created_at": filter_created_at,
"updated_at": filter_updated_at,
"start_date": filter_start_date,
"target_date": filter_target_date,
"completed_at": filter_completed_at,
"type": filter_issue_state_type,
"project": filter_project,
"cycle": filter_cycle,
"module": filter_module,
}
for key, value in ISSUE_FILTER.items():
if key in query_params:
func = value
func(query_params, filter, method)
return filter
+23
View File
@@ -0,0 +1,23 @@
# Python imports
import re
# Django imports
from django.db.models import Q
# Module imports
from plane.db.models import Issue
def search_issues(query):
fields = ["name", "sequence_id"]
q = Q()
for field in fields:
if field == "sequence_id":
sequences = re.findall(r"\d+\.\d+|\d+", query)
for sequence_id in sequences:
q |= Q(**{"sequence_id": sequence_id})
else:
q |= Q(**{f"{field}__icontains": query})
return Issue.objects.filter(
q,
).distinct()
+6 -4
View File
@@ -1,13 +1,13 @@
# base requirements
Django==3.2.18
Django==3.2.19
django-braces==1.15.0
django-taggit==3.1.0
psycopg2==2.9.5
django-oauth-toolkit==2.2.0
mistune==2.0.4
djangorestframework==3.14.0
redis==4.4.2
redis==4.5.4
django-nested-admin==4.0.2
django-cors-headers==3.13.0
whitenoise==6.3.0
@@ -23,7 +23,9 @@ django-guardian==2.4.0
dj_rest_auth==2.2.5
google-auth==2.16.0
google-api-python-client==2.75.0
django-rq==2.6.0
django-redis==5.2.0
uvicorn==0.20.0
channels==4.0.0
channels==4.0.0
openai==0.27.2
slack-sdk==3.20.2
celery==5.2.7

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