From 39eabc28b5d6d5a770a99f00b94421e7d45f5f93 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Mon, 7 Oct 2024 20:07:24 +0530 Subject: [PATCH 01/18] chore: only admin can changed the project settings (#5766) --- apiserver/plane/app/views/project/base.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py index 6a9afb65237..f5ddb224587 100644 --- a/apiserver/plane/app/views/project/base.py +++ b/apiserver/plane/app/views/project/base.py @@ -413,9 +413,20 @@ def create(self, request, slug): status=status.HTTP_410_GONE, ) - @allow_permission([ROLE.ADMIN]) def partial_update(self, request, slug, pk=None): try: + if not ProjectMember.objects.filter( + member=request.user, + workspace__slug=slug, + project_id=pk, + role=20, + is_active=True, + ).exists(): + return Response( + {"error": "You don't have the required permissions."}, + status=status.HTTP_403_FORBIDDEN, + ) + workspace = Workspace.objects.get(slug=slug) project = Project.objects.get(pk=pk) From 328b6961a26f8a0a40e630b6f9c4e0070928a32a Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Tue, 8 Oct 2024 13:20:27 +0530 Subject: [PATCH 02/18] [WEB-2605] fix: update URL regex pattern to allow complex links. (#5767) --- web/helpers/string.helper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/helpers/string.helper.ts b/web/helpers/string.helper.ts index 1182feeb0a7..e4b4dc6651e 100644 --- a/web/helpers/string.helper.ts +++ b/web/helpers/string.helper.ts @@ -270,7 +270,7 @@ export const isCommentEmpty = (comment: string | undefined): boolean => { export const checkURLValidity = (url: string): boolean => { if (!url) return false; // regex to match valid URLs (with or without http/https) - const urlPattern = /^(https?:\/\/)?([\da-z.-]+)\.([a-z]{2,6})(\/[\w.-]*)*\/?(\?[=&\w.-]*)?$/i; + const urlPattern = /^(https?:\/\/)?([\w.-]+\.[a-z]{2,6})(\/[\w\-.~:/?#[\]@!$&'()*+,;=%]*)?$/i; // test if the URL matches the pattern return urlPattern.test(url); }; From 5fb7e98b7caac28e240741ef9ade7af85ac770b4 Mon Sep 17 00:00:00 2001 From: "M. Palanikannan" <73993394+Palanikannan1437@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:44:05 +0530 Subject: [PATCH 03/18] fix: drag handle scrolling fixed (#5619) * fix: drag handle scrolling fixed * fix: closest scrollable parent found and scrolled * fix: removed overflow auto from framerenderer * fix: make dragging dynamic and smoother --- .../editor/src/core/extensions/side-menu.tsx | 2 +- .../editor/src/core/plugins/drag-handle.ts | 44 ++++++++++++++++--- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/packages/editor/src/core/extensions/side-menu.tsx b/packages/editor/src/core/extensions/side-menu.tsx index 616e315e206..5ab6fbdf5b3 100644 --- a/packages/editor/src/core/extensions/side-menu.tsx +++ b/packages/editor/src/core/extensions/side-menu.tsx @@ -42,7 +42,7 @@ export const SideMenuExtension = (props: Props) => { ai: aiEnabled, dragDrop: dragDropEnabled, }, - scrollThreshold: { up: 300, down: 100 }, + scrollThreshold: { up: 200, down: 100 }, }), ]; }, diff --git a/packages/editor/src/core/plugins/drag-handle.ts b/packages/editor/src/core/plugins/drag-handle.ts index ba390180bbe..809802b4f92 100644 --- a/packages/editor/src/core/plugins/drag-handle.ts +++ b/packages/editor/src/core/plugins/drag-handle.ts @@ -233,14 +233,46 @@ export const DragHandlePlugin = (options: SideMenuPluginProps): SideMenuHandleOp dragHandleElement.addEventListener("click", (e) => handleClick(e, view)); dragHandleElement.addEventListener("contextmenu", (e) => handleClick(e, view)); + const isScrollable = (node: HTMLElement | SVGElement) => { + if (!(node instanceof HTMLElement || node instanceof SVGElement)) { + return false; + } + const style = getComputedStyle(node); + return ["overflow", "overflow-y"].some((propertyName) => { + const value = style.getPropertyValue(propertyName); + return value === "auto" || value === "scroll"; + }); + }; + + const getScrollParent = (node: HTMLElement | SVGElement) => { + let currentParent = node.parentElement; + while (currentParent) { + if (isScrollable(currentParent)) { + return currentParent; + } + currentParent = currentParent.parentElement; + } + return document.scrollingElement || document.documentElement; + }; + + const maxScrollSpeed = 100; + dragHandleElement.addEventListener("drag", (e) => { hideDragHandle(); - const frameRenderer = document.querySelector(".frame-renderer"); - if (!frameRenderer) return; - if (e.clientY < options.scrollThreshold.up) { - frameRenderer.scrollBy({ top: -70, behavior: "smooth" }); - } else if (window.innerHeight - e.clientY < options.scrollThreshold.down) { - frameRenderer.scrollBy({ top: 70, behavior: "smooth" }); + const scrollableParent = getScrollParent(dragHandleElement); + if (!scrollableParent) return; + const scrollThreshold = options.scrollThreshold; + + if (e.clientY < scrollThreshold.up) { + const overflow = scrollThreshold.up - e.clientY; + const ratio = Math.min(overflow / scrollThreshold.up, 1); + const scrollAmount = -maxScrollSpeed * ratio; + scrollableParent.scrollBy({ top: scrollAmount }); + } else if (window.innerHeight - e.clientY < scrollThreshold.down) { + const overflow = e.clientY - (window.innerHeight - scrollThreshold.down); + const ratio = Math.min(overflow / scrollThreshold.down, 1); + const scrollAmount = maxScrollSpeed * ratio; + scrollableParent.scrollBy({ top: scrollAmount }); } }); From 6bf0e27b66ce164ce59a19506bc5635047996d72 Mon Sep 17 00:00:00 2001 From: Mihir <82092317+Jimmycutie@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:44:50 +0530 Subject: [PATCH 04/18] [WEB-2433] chore-Update name of the Layout (#5661) * Updated layout names * Corrected character casing for titles --- .../[projectId]/cycles/(detail)/mobile-header.tsx | 2 +- .../[projectId]/issues/(list)/mobile-header.tsx | 2 +- .../[projectId]/modules/(detail)/mobile-header.tsx | 2 +- web/core/constants/cycle.ts | 4 ++-- web/core/constants/issue.ts | 14 +++++++------- web/core/constants/module.ts | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx index 27e33e2c2c5..aa81ae5816a 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx @@ -29,7 +29,7 @@ export const CycleIssuesMobileHeader = () => { const { getCycleById } = useCycle(); const layouts = [ { key: "list", title: "List", icon: List }, - { key: "kanban", title: "Kanban", icon: Kanban }, + { key: "kanban", title: "Board", icon: Kanban }, { key: "calendar", title: "Calendar", icon: Calendar }, ]; diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/mobile-header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/mobile-header.tsx index a1255b206c2..025c9fab04f 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/mobile-header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/mobile-header.tsx @@ -28,7 +28,7 @@ import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/h export const ProjectIssuesMobileHeader = observer(() => { const layouts = [ { key: "list", title: "List", icon: List }, - { key: "kanban", title: "Kanban", icon: Kanban }, + { key: "kanban", title: "Board", icon: Kanban }, { key: "calendar", title: "Calendar", icon: Calendar }, ]; const [analyticsModal, setAnalyticsModal] = useState(false); diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/mobile-header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/mobile-header.tsx index f67df642c76..0edfa2b5a2b 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/mobile-header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/mobile-header.tsx @@ -31,7 +31,7 @@ export const ModuleIssuesMobileHeader = observer(() => { const { getModuleById } = useModule(); const layouts = [ { key: "list", title: "List", icon: List }, - { key: "kanban", title: "Kanban", icon: Kanban }, + { key: "kanban", title: "Board", icon: Kanban }, { key: "calendar", title: "Calendar", icon: Calendar }, ]; const { workspaceSlug, projectId, moduleId } = useParams() as { diff --git a/web/core/constants/cycle.ts b/web/core/constants/cycle.ts index 2db37136672..6f69e8f50a2 100644 --- a/web/core/constants/cycle.ts +++ b/web/core/constants/cycle.ts @@ -40,12 +40,12 @@ export const CYCLE_VIEW_LAYOUTS: { { key: "board", icon: LayoutGrid, - title: "Grid layout", + title: "Gallery layout", }, { key: "gantt", icon: GanttChartSquare, - title: "Gantt layout", + title: "Timeline layout", }, ]; diff --git a/web/core/constants/issue.ts b/web/core/constants/issue.ts index 3d00e34eba1..22dc8817a96 100644 --- a/web/core/constants/issue.ts +++ b/web/core/constants/issue.ts @@ -156,24 +156,24 @@ export const ISSUE_EXTRA_OPTIONS: { ]; export const ISSUE_LAYOUT_MAP = { - [EIssueLayoutTypes.LIST]: { key: EIssueLayoutTypes.LIST, title: "List Layout", label: "List", icon: List }, - [EIssueLayoutTypes.KANBAN]: { key: EIssueLayoutTypes.KANBAN, title: "Kanban Layout", label: "Kanban", icon: Kanban }, + [EIssueLayoutTypes.LIST]: { key: EIssueLayoutTypes.LIST, title: "List layout", label: "List", icon: List }, + [EIssueLayoutTypes.KANBAN]: { key: EIssueLayoutTypes.KANBAN, title: "Board layout", label: "Board", icon: Kanban }, [EIssueLayoutTypes.CALENDAR]: { key: EIssueLayoutTypes.CALENDAR, - title: "Calendar Layout", + title: "Calendar layout", label: "Calendar", icon: Calendar, }, [EIssueLayoutTypes.SPREADSHEET]: { key: EIssueLayoutTypes.SPREADSHEET, - title: "Spreadsheet Layout", - label: "Spreadsheet", + title: "Table layout", + label: "Table", icon: Sheet, }, [EIssueLayoutTypes.GANTT]: { key: EIssueLayoutTypes.GANTT, - title: "Gantt Chart Layout", - label: "Gantt", + title: "Timeline layout", + label: "Timeline", icon: GanttChartSquare, }, }; diff --git a/web/core/constants/module.ts b/web/core/constants/module.ts index bfec73dae08..8d26ab5f4e7 100644 --- a/web/core/constants/module.ts +++ b/web/core/constants/module.ts @@ -62,12 +62,12 @@ export const MODULE_VIEW_LAYOUTS: { key: TModuleLayoutOptions; icon: any; title: { key: "board", icon: LayoutGrid, - title: "Grid layout", + title: "Gallery layout", }, { key: "gantt", icon: GanttChartSquare, - title: "Gantt layout", + title: "Timeline layout", }, ]; From 39195d0d89a15cec5e4f806e5cde46ba75f6a924 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:47:16 +0530 Subject: [PATCH 05/18] [WEB-2532] fix: custom theme mutation logic (#5685) * fix: custom theme mutation logic * chore: update querySelector element --- web/app/profile/appearance/page.tsx | 9 ++------ web/core/lib/wrappers/store-wrapper.tsx | 30 +++---------------------- web/helpers/theme.helper.ts | 19 ++++++++-------- 3 files changed, 15 insertions(+), 43 deletions(-) diff --git a/web/app/profile/appearance/page.tsx b/web/app/profile/appearance/page.tsx index ef19a3342cf..775ff637b04 100644 --- a/web/app/profile/appearance/page.tsx +++ b/web/app/profile/appearance/page.tsx @@ -52,13 +52,8 @@ const ProfileAppearancePage = observer(() => { const applyThemeChange = (theme: Partial) => { setTheme(theme?.theme || "system"); - const customThemeElement = window.document?.querySelector("[data-theme='custom']"); - if (theme?.theme === "custom" && theme?.palette && customThemeElement) { - applyTheme( - theme?.palette !== ",,,," ? theme?.palette : "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5", - false, - customThemeElement - ); + if (theme?.theme === "custom" && theme?.palette) { + applyTheme(theme?.palette !== ",,,," ? theme?.palette : "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5", false); } else unsetCustomCssVariables(); }; diff --git a/web/core/lib/wrappers/store-wrapper.tsx b/web/core/lib/wrappers/store-wrapper.tsx index f12821c5a1d..fa60c354e3c 100644 --- a/web/core/lib/wrappers/store-wrapper.tsx +++ b/web/core/lib/wrappers/store-wrapper.tsx @@ -21,8 +21,6 @@ const StoreWrapper: FC = observer((props) => { const { setQuery } = useRouterParams(); const { sidebarCollapsed, toggleSidebar } = useAppTheme(); const { data: userProfile } = useUserProfile(); - // states - const [dom, setDom] = useState(null); /** * Sidebar collapsed fetching from local storage @@ -44,36 +42,14 @@ const StoreWrapper: FC = observer((props) => { const currentThemePalette = userProfile?.theme?.palette; if (currentTheme) { setTheme(currentTheme); - if (currentTheme === "custom" && currentThemePalette && dom) { + if (currentTheme === "custom" && currentThemePalette) { applyTheme( currentThemePalette !== ",,,," ? currentThemePalette : "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5", - false, - dom + false ); } else unsetCustomCssVariables(); } - }, [userProfile?.theme?.theme, userProfile?.theme?.palette, setTheme, dom]); - - useEffect(() => { - if (dom) return; - - const observer = new MutationObserver((mutationsList, observer) => { - for (const mutation of mutationsList) { - if (mutation.type === "childList") { - const customThemeElement = window.document?.querySelector("[data-theme='custom']"); - if (customThemeElement) { - setDom(customThemeElement); - observer.disconnect(); - break; - } - } - } - }); - - observer.observe(document.body, { childList: true, subtree: true }); - - return () => observer.disconnect(); - }, [dom]); + }, [userProfile?.theme?.theme, userProfile?.theme?.palette, setTheme]); useEffect(() => { if (!params) return; diff --git a/web/helpers/theme.helper.ts b/web/helpers/theme.helper.ts index fd3bd07afa4..9b0638c1f66 100644 --- a/web/helpers/theme.helper.ts +++ b/web/helpers/theme.helper.ts @@ -59,8 +59,9 @@ const calculateShades = (hexValue: string): TShades => { return shades as TShades; }; -export const applyTheme = (palette: string, isDarkPalette: boolean, dom: HTMLElement | null) => { +export const applyTheme = (palette: string, isDarkPalette: boolean) => { if (!palette) return; + const themeElement = document?.querySelector("html"); // palette: [bg, text, primary, sidebarBg, sidebarText] const values: string[] = palette.split(","); values.push(isDarkPalette ? "dark" : "light"); @@ -80,27 +81,27 @@ export const applyTheme = (palette: string, isDarkPalette: boolean, dom: HTMLEle const sidebarBackgroundRgbValues = `${sidebarBackgroundShades[shade].r}, ${sidebarBackgroundShades[shade].g}, ${sidebarBackgroundShades[shade].b}`; const sidebarTextRgbValues = `${sidebarTextShades[shade].r}, ${sidebarTextShades[shade].g}, ${sidebarTextShades[shade].b}`; - dom?.style.setProperty(`--color-background-${shade}`, bgRgbValues); - dom?.style.setProperty(`--color-text-${shade}`, textRgbValues); - dom?.style.setProperty(`--color-primary-${shade}`, primaryRgbValues); - dom?.style.setProperty(`--color-sidebar-background-${shade}`, sidebarBackgroundRgbValues); - dom?.style.setProperty(`--color-sidebar-text-${shade}`, sidebarTextRgbValues); + themeElement?.style.setProperty(`--color-background-${shade}`, bgRgbValues); + themeElement?.style.setProperty(`--color-text-${shade}`, textRgbValues); + themeElement?.style.setProperty(`--color-primary-${shade}`, primaryRgbValues); + themeElement?.style.setProperty(`--color-sidebar-background-${shade}`, sidebarBackgroundRgbValues); + themeElement?.style.setProperty(`--color-sidebar-text-${shade}`, sidebarTextRgbValues); if (i >= 100 && i <= 400) { const borderShade = i === 100 ? 70 : i === 200 ? 80 : i === 300 ? 90 : 100; - dom?.style.setProperty( + themeElement?.style.setProperty( `--color-border-${shade}`, `${bgShades[borderShade].r}, ${bgShades[borderShade].g}, ${bgShades[borderShade].b}` ); - dom?.style.setProperty( + themeElement?.style.setProperty( `--color-sidebar-border-${shade}`, `${sidebarBackgroundShades[borderShade].r}, ${sidebarBackgroundShades[borderShade].g}, ${sidebarBackgroundShades[borderShade].b}` ); } } - dom?.style.setProperty("--color-scheme", values[5]); + themeElement?.style.setProperty("--color-scheme", values[5]); }; export const unsetCustomCssVariables = () => { From 7317975b04b21d2387e412093292745323d0e95d Mon Sep 17 00:00:00 2001 From: "M. Palanikannan" <73993394+Palanikannan1437@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:50:32 +0530 Subject: [PATCH 06/18] fix: show the full screen toolbar in read only instances as well (#5746) --- .../extensions/custom-image/components/image-block.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/editor/src/core/extensions/custom-image/components/image-block.tsx b/packages/editor/src/core/extensions/custom-image/components/image-block.tsx index f067ee94780..ed60f3dab08 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-block.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-block.tsx @@ -201,8 +201,10 @@ export const CustomImageBlock: React.FC = (props) => { // show the image loader if the remote image's src or preview image from filesystem is not set yet (while loading the image post upload) (or) // if the initial resize (from 35% width and "auto" height attrs to the actual size in px) is not complete const showImageLoader = !(remoteImageSrc || imageFromFileSystem) || !initialResizeComplete; - // show the image utils only if the editor is editable, the remote image's (post upload) src is set and the initial resize is complete (but not while we're showing the preview imageFromFileSystem) - const showImageUtils = editor.isEditable && remoteImageSrc && initialResizeComplete; + // show the image utils only if the remote image's (post upload) src is set and the initial resize is complete (but not while we're showing the preview imageFromFileSystem) + const showImageUtils = remoteImageSrc && initialResizeComplete; + // show the image resizer only if the editor is editable, the remote image's (post upload) src is set and the initial resize is complete (but not while we're showing the preview imageFromFileSystem) + const showImageResizer = editor.isEditable && remoteImageSrc && initialResizeComplete; // show the preview image from the file system if the remote image's src is not set const displayedImageSrc = remoteImageSrc ?? imageFromFileSystem; @@ -258,7 +260,7 @@ export const CustomImageBlock: React.FC = (props) => { {selected && displayedImageSrc === remoteImageSrc && (
)} - {showImageUtils && ( + {showImageResizer && ( <>
Date: Tue, 8 Oct 2024 16:51:57 +0530 Subject: [PATCH 07/18] [WEB-2388] fix: workspace draft issues migration (#5749) * fix: workspace draft issues * chore: changed the timezone key * chore: migration changes --- apiserver/plane/api/views/cycle.py | 8 +- apiserver/plane/app/serializers/__init__.py | 6 + apiserver/plane/app/serializers/draft.py | 278 +++ apiserver/plane/app/urls/issue.py | 23 - apiserver/plane/app/urls/workspace.py | 22 + apiserver/plane/app/views/__init__.py | 4 +- apiserver/plane/app/views/cycle/archive.py | 2 +- apiserver/plane/app/views/cycle/base.py | 4 +- apiserver/plane/app/views/cycle/issue.py | 2 +- apiserver/plane/app/views/dashboard/base.py | 2 - apiserver/plane/app/views/issue/draft.py | 410 ---- apiserver/plane/app/views/workspace/draft.py | 236 ++ apiserver/plane/app/views/workspace/user.py | 6 +- .../plane/bgtasks/issue_automation_task.py | 12 +- ...timezone_project_user_timezone_and_more.py | 2036 +++++++++++++++++ apiserver/plane/db/models/__init__.py | 1 + apiserver/plane/db/models/cycle.py | 15 +- apiserver/plane/db/models/draft.py | 253 ++ apiserver/plane/db/models/project.py | 8 +- apiserver/plane/utils/analytics_plot.py | 4 +- 20 files changed, 2871 insertions(+), 461 deletions(-) create mode 100644 apiserver/plane/app/serializers/draft.py delete mode 100644 apiserver/plane/app/views/issue/draft.py create mode 100644 apiserver/plane/app/views/workspace/draft.py create mode 100644 apiserver/plane/db/migrations/0077_draftissue_cycle_user_timezone_project_user_timezone_and_more.py create mode 100644 apiserver/plane/db/models/draft.py diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index 3814466322e..be23256ac6d 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -207,7 +207,7 @@ def get(self, request, slug, project_id, pk=None): # Incomplete Cycles if cycle_view == "incomplete": queryset = queryset.filter( - Q(end_date__gte=timezone.now().date()) + Q(end_date__gte=timezone.now()) | Q(end_date__isnull=True), ) return self.paginate( @@ -311,7 +311,7 @@ def patch(self, request, slug, project_id, pk): if ( cycle.end_date is not None - and cycle.end_date < timezone.now().date() + and cycle.end_date < timezone.now() ): if "sort_order" in request_data: # Can only change sort order @@ -537,7 +537,7 @@ def post(self, request, slug, project_id, cycle_id): cycle = Cycle.objects.get( pk=cycle_id, project_id=project_id, workspace__slug=slug ) - if cycle.end_date >= timezone.now().date(): + if cycle.end_date >= timezone.now(): return Response( {"error": "Only completed cycles can be archived"}, status=status.HTTP_400_BAD_REQUEST, @@ -1146,7 +1146,7 @@ def post(self, request, slug, project_id, cycle_id): if ( new_cycle.end_date is not None - and new_cycle.end_date < timezone.now().date() + and new_cycle.end_date < timezone.now() ): return Response( { diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py index 618a9ec20f1..b3c3d794994 100644 --- a/apiserver/plane/app/serializers/__init__.py +++ b/apiserver/plane/app/serializers/__init__.py @@ -124,3 +124,9 @@ from .dashboard import DashboardSerializer, WidgetSerializer from .favorite import UserFavoriteSerializer + +from .draft import ( + DraftIssueCreateSerializer, + DraftIssueSerializer, + DraftIssueDetailSerializer, +) diff --git a/apiserver/plane/app/serializers/draft.py b/apiserver/plane/app/serializers/draft.py new file mode 100644 index 00000000000..1ca9a743128 --- /dev/null +++ b/apiserver/plane/app/serializers/draft.py @@ -0,0 +1,278 @@ +# Django imports +from django.utils import timezone + +# Third Party imports +from rest_framework import serializers + +# Module imports +from .base import BaseSerializer +from plane.db.models import ( + User, + Issue, + Label, + State, + DraftIssue, + DraftIssueAssignee, + DraftIssueLabel, + DraftIssueCycle, + DraftIssueModule, +) + + +class DraftIssueCreateSerializer(BaseSerializer): + # ids + state_id = serializers.PrimaryKeyRelatedField( + source="state", + queryset=State.objects.all(), + required=False, + allow_null=True, + ) + parent_id = serializers.PrimaryKeyRelatedField( + source="parent", + queryset=Issue.objects.all(), + required=False, + allow_null=True, + ) + label_ids = serializers.ListField( + child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()), + write_only=True, + required=False, + ) + assignee_ids = serializers.ListField( + child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()), + write_only=True, + required=False, + ) + + class Meta: + model = DraftIssue + fields = "__all__" + read_only_fields = [ + "workspace", + "project", + "created_by", + "updated_by", + "created_at", + "updated_at", + ] + + def to_representation(self, instance): + data = super().to_representation(instance) + assignee_ids = self.initial_data.get("assignee_ids") + data["assignee_ids"] = assignee_ids if assignee_ids else [] + label_ids = self.initial_data.get("label_ids") + data["label_ids"] = label_ids if label_ids else [] + return data + + def validate(self, data): + if ( + data.get("start_date", None) is not None + and data.get("target_date", None) is not None + and data.get("start_date", None) > data.get("target_date", None) + ): + raise serializers.ValidationError( + "Start date cannot exceed target date" + ) + return data + + def create(self, validated_data): + assignees = validated_data.pop("assignee_ids", None) + labels = validated_data.pop("label_ids", None) + modules = validated_data.pop("module_ids", None) + cycle_id = self.initial_data.get("cycle_id", None) + modules = self.initial_data.get("module_ids", None) + + workspace_id = self.context["workspace_id"] + + # Create Issue + issue = DraftIssue.objects.create( + **validated_data, + workspace_id=workspace_id, + ) + + # Issue Audit Users + created_by_id = issue.created_by_id + updated_by_id = issue.updated_by_id + + if assignees is not None and len(assignees): + DraftIssueAssignee.objects.bulk_create( + [ + DraftIssueAssignee( + assignee=user, + issue=issue, + workspace_id=workspace_id, + created_by_id=created_by_id, + updated_by_id=updated_by_id, + ) + for user in assignees + ], + batch_size=10, + ) + + if labels is not None and len(labels): + DraftIssueLabel.objects.bulk_create( + [ + DraftIssueLabel( + label=label, + issue=issue, + workspace_id=workspace_id, + created_by_id=created_by_id, + updated_by_id=updated_by_id, + ) + for label in labels + ], + batch_size=10, + ) + + if cycle_id is not None: + DraftIssueCycle.objects.create( + cycle_id=cycle_id, + draft_issue=issue, + workspace_id=workspace_id, + created_by_id=created_by_id, + updated_by_id=updated_by_id, + ) + + if modules is not None and len(modules): + DraftIssueModule.objects.bulk_create( + [ + DraftIssueModule( + module_id=module_id, + draft_issue=issue, + workspace_id=workspace_id, + created_by_id=created_by_id, + updated_by_id=updated_by_id, + ) + for module_id in modules + ], + batch_size=10, + ) + + return issue + + def update(self, instance, validated_data): + assignees = validated_data.pop("assignee_ids", None) + labels = validated_data.pop("label_ids", None) + cycle_id = self.initial_data.get("cycle_id", None) + modules = self.initial_data.get("module_ids", None) + + # Related models + workspace_id = instance.workspace_id + created_by_id = instance.created_by_id + updated_by_id = instance.updated_by_id + + if assignees is not None: + DraftIssueAssignee.objects.filter(issue=instance).delete() + DraftIssueAssignee.objects.bulk_create( + [ + DraftIssueAssignee( + assignee=user, + issue=instance, + workspace_id=workspace_id, + created_by_id=created_by_id, + updated_by_id=updated_by_id, + ) + for user in assignees + ], + batch_size=10, + ) + + if labels is not None: + DraftIssueLabel.objects.filter(issue=instance).delete() + DraftIssueLabel.objects.bulk_create( + [ + DraftIssueLabel( + label=label, + issue=instance, + workspace_id=workspace_id, + created_by_id=created_by_id, + updated_by_id=updated_by_id, + ) + for label in labels + ], + batch_size=10, + ) + + if cycle_id is not None: + DraftIssueCycle.objects.filter(draft_issue=instance).delete() + DraftIssueCycle.objects.create( + cycle_id=cycle_id, + draft_issue=instance, + workspace_id=workspace_id, + created_by_id=created_by_id, + updated_by_id=updated_by_id, + ) + + if modules is not None: + DraftIssueModule.objects.filter(draft_issue=instance).delete() + DraftIssueModule.objects.bulk_create( + [ + DraftIssueModule( + module=module, + draft_issue=instance, + workspace_id=workspace_id, + created_by_id=created_by_id, + updated_by_id=updated_by_id, + ) + for module in modules + ], + batch_size=10, + ) + + # Time updation occurs even when other related models are updated + instance.updated_at = timezone.now() + return super().update(instance, validated_data) + + +class DraftIssueSerializer(BaseSerializer): + # ids + cycle_id = serializers.PrimaryKeyRelatedField(read_only=True) + module_ids = serializers.ListField( + child=serializers.UUIDField(), + required=False, + ) + + # Many to many + label_ids = serializers.ListField( + child=serializers.UUIDField(), + required=False, + ) + assignee_ids = serializers.ListField( + child=serializers.UUIDField(), + required=False, + ) + + class Meta: + model = DraftIssue + fields = [ + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "created_at", + "updated_at", + "created_by", + "updated_by", + ] + read_only_fields = fields + + +class DraftIssueDetailSerializer(DraftIssueSerializer): + description_html = serializers.CharField() + + class Meta(DraftIssueSerializer.Meta): + fields = DraftIssueSerializer.Meta.fields + [ + "description_html", + ] + read_only_fields = fields diff --git a/apiserver/plane/app/urls/issue.py b/apiserver/plane/app/urls/issue.py index 564725e8391..e6007862fda 100644 --- a/apiserver/plane/app/urls/issue.py +++ b/apiserver/plane/app/urls/issue.py @@ -11,7 +11,6 @@ IssueActivityEndpoint, IssueArchiveViewSet, IssueCommentViewSet, - IssueDraftViewSet, IssueListEndpoint, IssueReactionViewSet, IssueRelationViewSet, @@ -290,28 +289,6 @@ name="issue-relation", ), ## End Issue Relation - ## Issue Drafts - path( - "workspaces//projects//issue-drafts/", - IssueDraftViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-issue-draft", - ), - path( - "workspaces//projects//issue-drafts//", - IssueDraftViewSet.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project-issue-draft", - ), path( "workspaces//projects//deleted-issues/", DeletedIssuesListViewSet.as_view(), diff --git a/apiserver/plane/app/urls/workspace.py b/apiserver/plane/app/urls/workspace.py index 3f1e000e473..7dab175441c 100644 --- a/apiserver/plane/app/urls/workspace.py +++ b/apiserver/plane/app/urls/workspace.py @@ -27,6 +27,7 @@ WorkspaceCyclesEndpoint, WorkspaceFavoriteEndpoint, WorkspaceFavoriteGroupEndpoint, + WorkspaceDraftIssueViewSet, ) @@ -254,4 +255,25 @@ WorkspaceFavoriteGroupEndpoint.as_view(), name="workspace-user-favorites-groups", ), + path( + "workspaces//draft-issues/", + WorkspaceDraftIssueViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="workspace-draft-issues", + ), + path( + "workspaces//draft-issues//", + WorkspaceDraftIssueViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="workspace-drafts-issues", + ), ] diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 6c4cc12c894..872b511a049 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -40,6 +40,8 @@ ExportWorkspaceUserActivityEndpoint, ) +from .workspace.draft import WorkspaceDraftIssueViewSet + from .workspace.favorite import ( WorkspaceFavoriteEndpoint, WorkspaceFavoriteGroupEndpoint, @@ -133,8 +135,6 @@ CommentReactionViewSet, ) -from .issue.draft import IssueDraftViewSet - from .issue.label import ( LabelViewSet, BulkCreateIssueLabelsEndpoint, diff --git a/apiserver/plane/app/views/cycle/archive.py b/apiserver/plane/app/views/cycle/archive.py index 25ad8a2eb6e..9d7f79b0ec1 100644 --- a/apiserver/plane/app/views/cycle/archive.py +++ b/apiserver/plane/app/views/cycle/archive.py @@ -604,7 +604,7 @@ def post(self, request, slug, project_id, cycle_id): pk=cycle_id, project_id=project_id, workspace__slug=slug ) - if cycle.end_date >= timezone.now().date(): + if cycle.end_date >= timezone.now(): return Response( {"error": "Only completed cycles can be archived"}, status=status.HTTP_400_BAD_REQUEST, diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index fc04abe35d2..df1bd93feba 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -308,7 +308,7 @@ def partial_update(self, request, slug, project_id, pk): if ( cycle.end_date is not None - and cycle.end_date < timezone.now().date() + and cycle.end_date < timezone.now() ): if "sort_order" in request_data: # Can only change sort order for a completed cycle`` @@ -925,7 +925,7 @@ def post(self, request, slug, project_id, cycle_id): if ( new_cycle.end_date is not None - and new_cycle.end_date < timezone.now().date() + and new_cycle.end_date < timezone.now() ): return Response( { diff --git a/apiserver/plane/app/views/cycle/issue.py b/apiserver/plane/app/views/cycle/issue.py index 211f5a88acf..6be2c9ea943 100644 --- a/apiserver/plane/app/views/cycle/issue.py +++ b/apiserver/plane/app/views/cycle/issue.py @@ -248,7 +248,7 @@ def create(self, request, slug, project_id, cycle_id): if ( cycle.end_date is not None - and cycle.end_date < timezone.now().date() + and cycle.end_date < timezone.now() ): return Response( { diff --git a/apiserver/plane/app/views/dashboard/base.py b/apiserver/plane/app/views/dashboard/base.py index 4a760ca3b14..6a72fed2898 100644 --- a/apiserver/plane/app/views/dashboard/base.py +++ b/apiserver/plane/app/views/dashboard/base.py @@ -40,8 +40,6 @@ IssueLink, IssueRelation, Project, - ProjectMember, - User, Widget, WorkspaceMember, ) diff --git a/apiserver/plane/app/views/issue/draft.py b/apiserver/plane/app/views/issue/draft.py deleted file mode 100644 index c5899d9725d..00000000000 --- a/apiserver/plane/app/views/issue/draft.py +++ /dev/null @@ -1,410 +0,0 @@ -# Python imports -import json - -# Django imports -from django.contrib.postgres.aggregates import ArrayAgg -from django.contrib.postgres.fields import ArrayField -from django.core.serializers.json import DjangoJSONEncoder -from django.db.models import ( - Exists, - F, - Func, - OuterRef, - Prefetch, - Q, - UUIDField, - Value, -) -from django.db.models.functions import Coalesce -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 import status -from rest_framework.response import Response - -# Module imports -from plane.app.permissions import ProjectEntityPermission -from plane.app.serializers import ( - IssueCreateSerializer, - IssueDetailSerializer, - IssueFlatSerializer, - IssueSerializer, -) -from plane.bgtasks.issue_activities_task import issue_activity -from plane.db.models import ( - Issue, - IssueAttachment, - IssueLink, - IssueReaction, - IssueSubscriber, - Project, - ProjectMember, -) -from plane.utils.grouper import ( - issue_group_values, - issue_on_results, - issue_queryset_grouper, -) -from plane.utils.issue_filters import issue_filters -from plane.utils.order_queryset import order_issue_queryset -from plane.utils.paginator import ( - GroupedOffsetPaginator, - SubGroupedOffsetPaginator, -) -from .. import BaseViewSet - - -class IssueDraftViewSet(BaseViewSet): - permission_classes = [ - ProjectEntityPermission, - ] - serializer_class = IssueFlatSerializer - model = Issue - - def get_queryset(self): - return ( - Issue.objects.filter(project_id=self.kwargs.get("project_id")) - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(is_draft=True) - .filter(deleted_at__isnull=True) - .select_related("workspace", "project", "state", "parent") - .prefetch_related("assignees", "labels", "issue_module__module") - .annotate(cycle_id=F("issue_cycle__cycle_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") - ) - .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - ).distinct() - - @method_decorator(gzip_page) - def list(self, request, slug, project_id): - filters = issue_filters(request.query_params, "GET") - - order_by_param = request.GET.get("order_by", "-created_at") - - issue_queryset = self.get_queryset().filter(**filters) - # Issue queryset - issue_queryset, order_by_param = order_issue_queryset( - issue_queryset=issue_queryset, - order_by_param=order_by_param, - ) - - # Group by - group_by = request.GET.get("group_by", False) - sub_group_by = request.GET.get("sub_group_by", False) - - # issue queryset - issue_queryset = issue_queryset_grouper( - queryset=issue_queryset, - group_by=group_by, - sub_group_by=sub_group_by, - ) - - if group_by: - # Check group and sub group value paginate - if sub_group_by: - if group_by == sub_group_by: - return Response( - { - "error": "Group by and sub group by cannot have same parameters" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - else: - # group and sub group pagination - return self.paginate( - request=request, - order_by=order_by_param, - queryset=issue_queryset, - on_results=lambda issues: issue_on_results( - group_by=group_by, - issues=issues, - sub_group_by=sub_group_by, - ), - paginator_cls=SubGroupedOffsetPaginator, - group_by_fields=issue_group_values( - field=group_by, - slug=slug, - project_id=project_id, - filters=filters, - ), - sub_group_by_fields=issue_group_values( - field=sub_group_by, - slug=slug, - project_id=project_id, - filters=filters, - ), - group_by_field_name=group_by, - sub_group_by_field_name=sub_group_by, - count_filter=Q( - Q(issue_inbox__status=1) - | Q(issue_inbox__status=-1) - | Q(issue_inbox__status=2) - | Q(issue_inbox__isnull=True), - archived_at__isnull=True, - is_draft=False, - ), - ) - # Group Paginate - else: - # Group paginate - return self.paginate( - request=request, - order_by=order_by_param, - queryset=issue_queryset, - on_results=lambda issues: issue_on_results( - group_by=group_by, - issues=issues, - sub_group_by=sub_group_by, - ), - paginator_cls=GroupedOffsetPaginator, - group_by_fields=issue_group_values( - field=group_by, - slug=slug, - project_id=project_id, - filters=filters, - ), - group_by_field_name=group_by, - count_filter=Q( - Q(issue_inbox__status=1) - | Q(issue_inbox__status=-1) - | Q(issue_inbox__status=2) - | Q(issue_inbox__isnull=True), - archived_at__isnull=True, - is_draft=False, - ), - ) - else: - # List Paginate - return self.paginate( - order_by=order_by_param, - request=request, - queryset=issue_queryset, - on_results=lambda issues: issue_on_results( - group_by=group_by, issues=issues, sub_group_by=sub_group_by - ), - ) - - def create(self, request, slug, project_id): - project = Project.objects.get(pk=project_id) - - serializer = IssueCreateSerializer( - data=request.data, - context={ - "project_id": project_id, - "workspace_id": project.workspace_id, - "default_assignee_id": project.default_assignee_id, - }, - ) - - if serializer.is_valid(): - serializer.save(is_draft=True) - - # Track the issue - issue_activity.delay( - type="issue_draft.activity.created", - requested_data=json.dumps( - self.request.data, cls=DjangoJSONEncoder - ), - actor_id=str(request.user.id), - issue_id=str(serializer.data.get("id", None)), - project_id=str(project_id), - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - - issue = ( - issue_queryset_grouper( - queryset=self.get_queryset().filter( - pk=serializer.data["id"] - ), - group_by=None, - sub_group_by=None, - ) - .values( - "id", - "name", - "state_id", - "sort_order", - "completed_at", - "estimate_point", - "priority", - "start_date", - "target_date", - "sequence_id", - "project_id", - "parent_id", - "cycle_id", - "module_ids", - "label_ids", - "assignee_ids", - "sub_issues_count", - "created_at", - "updated_at", - "created_by", - "updated_by", - "attachment_count", - "link_count", - "is_draft", - "archived_at", - ) - .first() - ) - return Response(issue, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def partial_update(self, request, slug, project_id, pk): - issue = self.get_queryset().filter(pk=pk).first() - - if not issue: - return Response( - {"error": "Issue does not exist"}, - status=status.HTTP_404_NOT_FOUND, - ) - - serializer = IssueCreateSerializer( - issue, data=request.data, partial=True - ) - - if serializer.is_valid(): - serializer.save() - issue_activity.delay( - type="issue_draft.activity.updated", - requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), - 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(issue).data, - cls=DjangoJSONEncoder, - ), - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(status=status.HTTP_204_NO_CONTENT) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def retrieve(self, request, slug, project_id, pk=None): - issue = ( - self.get_queryset() - .filter(pk=pk) - .annotate( - label_ids=Coalesce( - ArrayAgg( - "labels__id", - distinct=True, - filter=~Q(labels__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - assignee_ids=Coalesce( - ArrayAgg( - "assignees__id", - distinct=True, - filter=~Q(assignees__id__isnull=True) - & Q(assignees__member_project__is_active=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - module_ids=Coalesce( - ArrayAgg( - "issue_module__module_id", - distinct=True, - filter=~Q(issue_module__module_id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - ) - .prefetch_related( - Prefetch( - "issue_reactions", - queryset=IssueReaction.objects.select_related( - "issue", "actor" - ), - ) - ) - .prefetch_related( - Prefetch( - "issue_attachment", - queryset=IssueAttachment.objects.select_related("issue"), - ) - ) - .prefetch_related( - Prefetch( - "issue_link", - queryset=IssueLink.objects.select_related("created_by"), - ) - ) - .annotate( - is_subscribed=Exists( - IssueSubscriber.objects.filter( - workspace__slug=slug, - project_id=project_id, - issue_id=OuterRef("pk"), - subscriber=request.user, - ) - ) - ) - ).first() - - if not issue: - return Response( - {"error": "The required object does not exist."}, - status=status.HTTP_404_NOT_FOUND, - ) - serializer = IssueDetailSerializer(issue, expand=self.expand) - return Response(serializer.data, status=status.HTTP_200_OK) - - def destroy(self, request, slug, project_id, pk=None): - issue = Issue.objects.get( - workspace__slug=slug, project_id=project_id, pk=pk - ) - if issue.created_by_id != request.user.id and ( - not ProjectMember.objects.filter( - workspace__slug=slug, - member=request.user, - role=20, - project_id=project_id, - is_active=True, - ).exists() - ): - return Response( - {"error": "Only admin or creator can delete the issue"}, - status=status.HTTP_403_FORBIDDEN, - ) - issue.delete() - issue_activity.delay( - type="issue_draft.activity.deleted", - requested_data=json.dumps({"issue_id": str(pk)}), - actor_id=str(request.user.id), - issue_id=str(pk), - project_id=str(project_id), - current_instance={}, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/workspace/draft.py b/apiserver/plane/app/views/workspace/draft.py new file mode 100644 index 00000000000..8ec09d1482a --- /dev/null +++ b/apiserver/plane/app/views/workspace/draft.py @@ -0,0 +1,236 @@ +# Django imports +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.fields import ArrayField +from django.db.models import ( + F, + Q, + UUIDField, + Value, +) +from django.db.models.functions import Coalesce +from django.utils.decorators import method_decorator +from django.views.decorators.gzip import gzip_page + +# Third Party imports +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.app.permissions import allow_permission, ROLE +from plane.app.serializers import ( + IssueCreateSerializer, + DraftIssueCreateSerializer, + DraftIssueSerializer, + DraftIssueDetailSerializer, +) +from plane.db.models import ( + Issue, + DraftIssue, + Workspace, +) +from .. import BaseViewSet + + +class WorkspaceDraftIssueViewSet(BaseViewSet): + + model = DraftIssue + + @method_decorator(gzip_page) + @allow_permission( + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE" + ) + def list(self, request, slug): + issues = ( + DraftIssue.objects.filter(workspace__slug=slug) + .filter(created_by=request.user) + .select_related("workspace", "project", "state", "parent") + .prefetch_related( + "assignees", "labels", "draft_issue_module__module" + ) + .annotate(cycle_id=F("draft_issue_cycle__cycle_id")) + .annotate( + label_ids=Coalesce( + ArrayAgg( + "labels__id", + distinct=True, + filter=~Q(labels__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + assignee_ids=Coalesce( + ArrayAgg( + "assignees__id", + distinct=True, + filter=~Q(assignees__id__isnull=True) + & Q(assignees__member_project__is_active=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "draft_issue_module__module_id", + distinct=True, + filter=~Q(draft_issue_module__module_id__isnull=True) + & Q( + draft_issue_module__module__archived_at__isnull=True + ), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) + .order_by("-created_at") + ) + + serializer = DraftIssueSerializer(issues, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + @allow_permission( + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE" + ) + def create(self, request, slug): + workspace = Workspace.objects.get(slug=slug) + + serializer = DraftIssueCreateSerializer( + data=request.data, + context={ + "workspace_id": workspace.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) + + @allow_permission( + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], + creator=True, + model=Issue, + level="WORKSPACE", + ) + def partial_update(self, request, slug, pk): + issue = ( + DraftIssue.objects.filter(workspace__slug=slug) + .filter(pk=pk) + .filter(created_by=request.user) + .select_related("workspace", "project", "state", "parent") + .prefetch_related( + "assignees", "labels", "draft_issue_module__module" + ) + .annotate(cycle_id=F("draft_issue_cycle__cycle_id")) + .annotate( + label_ids=Coalesce( + ArrayAgg( + "labels__id", + distinct=True, + filter=~Q(labels__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + assignee_ids=Coalesce( + ArrayAgg( + "assignees__id", + distinct=True, + filter=~Q(assignees__id__isnull=True) + & Q(assignees__member_project__is_active=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "draft_issue_module__module_id", + distinct=True, + filter=~Q(draft_issue_module__module_id__isnull=True) + & Q( + draft_issue_module__module__archived_at__isnull=True + ), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) + .first() + ) + + if not issue: + return Response( + {"error": "Issue not found"}, + status=status.HTTP_404_NOT_FOUND, + ) + + serializer = IssueCreateSerializer( + issue, data=request.data, partial=True + ) + + if serializer.is_valid(): + serializer.save() + + return Response(status=status.HTTP_204_NO_CONTENT) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + @allow_permission( + allowed_roles=[ROLE.ADMIN], + creator=True, + model=Issue, + level="WORKSPACE", + ) + def retrieve(self, request, slug, pk=None): + issue = ( + DraftIssue.objects.filter(workspace__slug=slug) + .filter(pk=pk) + .filter(created_by=request.user) + .select_related("workspace", "project", "state", "parent") + .prefetch_related( + "assignees", "labels", "draft_issue_module__module" + ) + .annotate(cycle_id=F("draft_issue_cycle__cycle_id")) + .filter(pk=pk) + .annotate( + label_ids=Coalesce( + ArrayAgg( + "labels__id", + distinct=True, + filter=~Q(labels__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + assignee_ids=Coalesce( + ArrayAgg( + "assignees__id", + distinct=True, + filter=~Q(assignees__id__isnull=True) + & Q(assignees__member_project__is_active=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "draft_issue_module__module_id", + distinct=True, + filter=~Q(draft_issue_module__module_id__isnull=True) + & Q( + draft_issue_module__module__archived_at__isnull=True + ), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) + ).first() + + if not issue: + return Response( + {"error": "The required object does not exist."}, + status=status.HTTP_404_NOT_FOUND, + ) + + serializer = DraftIssueDetailSerializer(issue) + return Response(serializer.data, status=status.HTTP_200_OK) + + @allow_permission( + allowed_roles=[ROLE.ADMIN], + creator=True, + model=Issue, + level="WORKSPACE", + ) + def destroy(self, request, slug, pk=None): + draft_issue = DraftIssue.objects.get(workspace__slug=slug, pk=pk) + draft_issue.delete() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/workspace/user.py b/apiserver/plane/app/views/workspace/user.py index 5c173f2021f..15e2d01da03 100644 --- a/apiserver/plane/app/views/workspace/user.py +++ b/apiserver/plane/app/views/workspace/user.py @@ -504,7 +504,7 @@ def get(self, request, slug, user_id): upcoming_cycles = CycleIssue.objects.filter( workspace__slug=slug, - cycle__start_date__gt=timezone.now().date(), + cycle__start_date__gt=timezone.now(), issue__assignees__in=[ user_id, ], @@ -512,8 +512,8 @@ def get(self, request, slug, user_id): present_cycle = CycleIssue.objects.filter( workspace__slug=slug, - cycle__start_date__lt=timezone.now().date(), - cycle__end_date__gt=timezone.now().date(), + cycle__start_date__lt=timezone.now(), + cycle__end_date__gt=timezone.now(), issue__assignees__in=[ user_id, ], diff --git a/apiserver/plane/bgtasks/issue_automation_task.py b/apiserver/plane/bgtasks/issue_automation_task.py index 8e648c16b0b..e7ca16a984b 100644 --- a/apiserver/plane/bgtasks/issue_automation_task.py +++ b/apiserver/plane/bgtasks/issue_automation_task.py @@ -42,14 +42,12 @@ def archive_old_issues(): ), Q(issue_cycle__isnull=True) | ( - Q(issue_cycle__cycle__end_date__lt=timezone.now().date()) + Q(issue_cycle__cycle__end_date__lt=timezone.now()) & Q(issue_cycle__isnull=False) ), Q(issue_module__isnull=True) | ( - Q( - issue_module__module__target_date__lt=timezone.now().date() - ) + Q(issue_module__module__target_date__lt=timezone.now()) & Q(issue_module__isnull=False) ), ).filter( @@ -122,14 +120,12 @@ def close_old_issues(): ), Q(issue_cycle__isnull=True) | ( - Q(issue_cycle__cycle__end_date__lt=timezone.now().date()) + Q(issue_cycle__cycle__end_date__lt=timezone.now()) & Q(issue_cycle__isnull=False) ), Q(issue_module__isnull=True) | ( - Q( - issue_module__module__target_date__lt=timezone.now().date() - ) + Q(issue_module__module__target_date__lt=timezone.now()) & Q(issue_module__isnull=False) ), ).filter( diff --git a/apiserver/plane/db/migrations/0077_draftissue_cycle_user_timezone_project_user_timezone_and_more.py b/apiserver/plane/db/migrations/0077_draftissue_cycle_user_timezone_project_user_timezone_and_more.py new file mode 100644 index 00000000000..ee70f661528 --- /dev/null +++ b/apiserver/plane/db/migrations/0077_draftissue_cycle_user_timezone_project_user_timezone_and_more.py @@ -0,0 +1,2036 @@ +# Generated by Django 4.2.15 on 2024-09-24 08:44 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid +from django.db.models import Prefetch + + +def migrate_draft_issues(apps, schema_editor): + Issue = apps.get_model("db", "Issue") + DraftIssue = apps.get_model("db", "DraftIssue") + IssueAssignee = apps.get_model("db", "IssueAssignee") + DraftIssueAssignee = apps.get_model("db", "DraftIssueAssignee") + IssueLabel = apps.get_model("db", "IssueLabel") + DraftIssueLabel = apps.get_model("db", "DraftIssueLabel") + ModuleIssue = apps.get_model("db", "ModuleIssue") + DraftIssueModule = apps.get_model("db", "DraftIssueModule") + DraftIssueCycle = apps.get_model("db", "DraftIssueCycle") + + # Fetch all draft issues with their related assignees and labels + issues = ( + Issue.objects.filter(is_draft=True) + .select_related("issue_cycle__cycle") + .prefetch_related( + Prefetch( + "issue_assignee", + queryset=IssueAssignee.objects.select_related("assignee"), + ), + Prefetch( + "label_issue", + queryset=IssueLabel.objects.select_related("label"), + ), + Prefetch( + "issue_module", + queryset=ModuleIssue.objects.select_related("module"), + ), + ) + ) + + draft_issues = [] + draft_issue_cycle = [] + draft_issue_labels = [] + draft_issue_modules = [] + draft_issue_assignees = [] + # issue_ids_to_delete = [] + + for issue in issues: + draft_issue = DraftIssue( + parent_id=issue.parent_id, + state_id=issue.state_id, + estimate_point_id=issue.estimate_point_id, + name=issue.name, + description=issue.description, + description_html=issue.description_html, + description_stripped=issue.description_stripped, + description_binary=issue.description_binary, + priority=issue.priority, + start_date=issue.start_date, + target_date=issue.target_date, + workspace_id=issue.workspace_id, + project_id=issue.project_id, + created_by_id=issue.created_by_id, + updated_by_id=issue.updated_by_id, + ) + draft_issues.append(draft_issue) + + for assignee in issue.issue_assignee.all(): + draft_issue_assignees.append( + DraftIssueAssignee( + draft_issue=draft_issue, + assignee=assignee.assignee, + workspace_id=issue.workspace_id, + project_id=issue.project_id, + ) + ) + + # Prepare labels for bulk insert + for label in issue.label_issue.all(): + draft_issue_labels.append( + DraftIssueLabel( + draft_issue=draft_issue, + label=label.label, + workspace_id=issue.workspace_id, + project_id=issue.project_id, + ) + ) + + for module_issue in issue.issue_module.all(): + draft_issue_modules.append( + DraftIssueModule( + draft_issue=draft_issue, + module=module_issue.module, + workspace_id=issue.workspace_id, + project_id=issue.project_id, + ) + ) + + if hasattr(issue, "issue_cycle") and issue.issue_cycle: + draft_issue_cycle.append( + DraftIssueCycle( + draft_issue=draft_issue, + cycle=issue.issue_cycle.cycle, + workspace_id=issue.workspace_id, + project_id=issue.project_id, + ) + ) + + # issue_ids_to_delete.append(issue.id) + + # Bulk create draft issues + DraftIssue.objects.bulk_create(draft_issues) + + # Bulk create draft assignees and labels + DraftIssueLabel.objects.bulk_create(draft_issue_labels) + DraftIssueAssignee.objects.bulk_create(draft_issue_assignees) + + # Bulk create draft modules + DraftIssueCycle.objects.bulk_create(draft_issue_cycle) + DraftIssueModule.objects.bulk_create(draft_issue_modules) + + # Delete original issues + # Issue.objects.filter(id__in=issue_ids_to_delete).delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ("db", "0076_alter_projectmember_role_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="DraftIssue", + 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" + ), + ), + ( + "deleted_at", + models.DateTimeField( + blank=True, null=True, verbose_name="Deleted At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "name", + models.CharField( + blank=True, + max_length=255, + null=True, + verbose_name="Issue Name", + ), + ), + ("description", models.JSONField(blank=True, default=dict)), + ( + "description_html", + models.TextField(blank=True, default="

"), + ), + ( + "description_stripped", + models.TextField(blank=True, null=True), + ), + ("description_binary", models.BinaryField(null=True)), + ( + "priority", + models.CharField( + choices=[ + ("urgent", "Urgent"), + ("high", "High"), + ("medium", "Medium"), + ("low", "Low"), + ("none", "None"), + ], + default="none", + max_length=30, + verbose_name="Issue Priority", + ), + ), + ("start_date", models.DateField(blank=True, null=True)), + ("target_date", models.DateField(blank=True, null=True)), + ("sort_order", models.FloatField(default=65535)), + ("completed_at", models.DateTimeField(null=True)), + ( + "external_source", + models.CharField(blank=True, max_length=255, null=True), + ), + ( + "external_id", + models.CharField(blank=True, max_length=255, null=True), + ), + ], + options={ + "verbose_name": "DraftIssue", + "verbose_name_plural": "DraftIssues", + "db_table": "draft_issues", + "ordering": ("-created_at",), + }, + ), + migrations.AddField( + model_name="cycle", + name="timezone", + field=models.CharField( + choices=[ + ("Africa/Abidjan", "Africa/Abidjan"), + ("Africa/Accra", "Africa/Accra"), + ("Africa/Addis_Ababa", "Africa/Addis_Ababa"), + ("Africa/Algiers", "Africa/Algiers"), + ("Africa/Asmara", "Africa/Asmara"), + ("Africa/Asmera", "Africa/Asmera"), + ("Africa/Bamako", "Africa/Bamako"), + ("Africa/Bangui", "Africa/Bangui"), + ("Africa/Banjul", "Africa/Banjul"), + ("Africa/Bissau", "Africa/Bissau"), + ("Africa/Blantyre", "Africa/Blantyre"), + ("Africa/Brazzaville", "Africa/Brazzaville"), + ("Africa/Bujumbura", "Africa/Bujumbura"), + ("Africa/Cairo", "Africa/Cairo"), + ("Africa/Casablanca", "Africa/Casablanca"), + ("Africa/Ceuta", "Africa/Ceuta"), + ("Africa/Conakry", "Africa/Conakry"), + ("Africa/Dakar", "Africa/Dakar"), + ("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"), + ("Africa/Djibouti", "Africa/Djibouti"), + ("Africa/Douala", "Africa/Douala"), + ("Africa/El_Aaiun", "Africa/El_Aaiun"), + ("Africa/Freetown", "Africa/Freetown"), + ("Africa/Gaborone", "Africa/Gaborone"), + ("Africa/Harare", "Africa/Harare"), + ("Africa/Johannesburg", "Africa/Johannesburg"), + ("Africa/Juba", "Africa/Juba"), + ("Africa/Kampala", "Africa/Kampala"), + ("Africa/Khartoum", "Africa/Khartoum"), + ("Africa/Kigali", "Africa/Kigali"), + ("Africa/Kinshasa", "Africa/Kinshasa"), + ("Africa/Lagos", "Africa/Lagos"), + ("Africa/Libreville", "Africa/Libreville"), + ("Africa/Lome", "Africa/Lome"), + ("Africa/Luanda", "Africa/Luanda"), + ("Africa/Lubumbashi", "Africa/Lubumbashi"), + ("Africa/Lusaka", "Africa/Lusaka"), + ("Africa/Malabo", "Africa/Malabo"), + ("Africa/Maputo", "Africa/Maputo"), + ("Africa/Maseru", "Africa/Maseru"), + ("Africa/Mbabane", "Africa/Mbabane"), + ("Africa/Mogadishu", "Africa/Mogadishu"), + ("Africa/Monrovia", "Africa/Monrovia"), + ("Africa/Nairobi", "Africa/Nairobi"), + ("Africa/Ndjamena", "Africa/Ndjamena"), + ("Africa/Niamey", "Africa/Niamey"), + ("Africa/Nouakchott", "Africa/Nouakchott"), + ("Africa/Ouagadougou", "Africa/Ouagadougou"), + ("Africa/Porto-Novo", "Africa/Porto-Novo"), + ("Africa/Sao_Tome", "Africa/Sao_Tome"), + ("Africa/Timbuktu", "Africa/Timbuktu"), + ("Africa/Tripoli", "Africa/Tripoli"), + ("Africa/Tunis", "Africa/Tunis"), + ("Africa/Windhoek", "Africa/Windhoek"), + ("America/Adak", "America/Adak"), + ("America/Anchorage", "America/Anchorage"), + ("America/Anguilla", "America/Anguilla"), + ("America/Antigua", "America/Antigua"), + ("America/Araguaina", "America/Araguaina"), + ( + "America/Argentina/Buenos_Aires", + "America/Argentina/Buenos_Aires", + ), + ( + "America/Argentina/Catamarca", + "America/Argentina/Catamarca", + ), + ( + "America/Argentina/ComodRivadavia", + "America/Argentina/ComodRivadavia", + ), + ("America/Argentina/Cordoba", "America/Argentina/Cordoba"), + ("America/Argentina/Jujuy", "America/Argentina/Jujuy"), + ( + "America/Argentina/La_Rioja", + "America/Argentina/La_Rioja", + ), + ("America/Argentina/Mendoza", "America/Argentina/Mendoza"), + ( + "America/Argentina/Rio_Gallegos", + "America/Argentina/Rio_Gallegos", + ), + ("America/Argentina/Salta", "America/Argentina/Salta"), + ( + "America/Argentina/San_Juan", + "America/Argentina/San_Juan", + ), + ( + "America/Argentina/San_Luis", + "America/Argentina/San_Luis", + ), + ("America/Argentina/Tucuman", "America/Argentina/Tucuman"), + ("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"), + ("America/Aruba", "America/Aruba"), + ("America/Asuncion", "America/Asuncion"), + ("America/Atikokan", "America/Atikokan"), + ("America/Atka", "America/Atka"), + ("America/Bahia", "America/Bahia"), + ("America/Bahia_Banderas", "America/Bahia_Banderas"), + ("America/Barbados", "America/Barbados"), + ("America/Belem", "America/Belem"), + ("America/Belize", "America/Belize"), + ("America/Blanc-Sablon", "America/Blanc-Sablon"), + ("America/Boa_Vista", "America/Boa_Vista"), + ("America/Bogota", "America/Bogota"), + ("America/Boise", "America/Boise"), + ("America/Buenos_Aires", "America/Buenos_Aires"), + ("America/Cambridge_Bay", "America/Cambridge_Bay"), + ("America/Campo_Grande", "America/Campo_Grande"), + ("America/Cancun", "America/Cancun"), + ("America/Caracas", "America/Caracas"), + ("America/Catamarca", "America/Catamarca"), + ("America/Cayenne", "America/Cayenne"), + ("America/Cayman", "America/Cayman"), + ("America/Chicago", "America/Chicago"), + ("America/Chihuahua", "America/Chihuahua"), + ("America/Ciudad_Juarez", "America/Ciudad_Juarez"), + ("America/Coral_Harbour", "America/Coral_Harbour"), + ("America/Cordoba", "America/Cordoba"), + ("America/Costa_Rica", "America/Costa_Rica"), + ("America/Creston", "America/Creston"), + ("America/Cuiaba", "America/Cuiaba"), + ("America/Curacao", "America/Curacao"), + ("America/Danmarkshavn", "America/Danmarkshavn"), + ("America/Dawson", "America/Dawson"), + ("America/Dawson_Creek", "America/Dawson_Creek"), + ("America/Denver", "America/Denver"), + ("America/Detroit", "America/Detroit"), + ("America/Dominica", "America/Dominica"), + ("America/Edmonton", "America/Edmonton"), + ("America/Eirunepe", "America/Eirunepe"), + ("America/El_Salvador", "America/El_Salvador"), + ("America/Ensenada", "America/Ensenada"), + ("America/Fort_Nelson", "America/Fort_Nelson"), + ("America/Fort_Wayne", "America/Fort_Wayne"), + ("America/Fortaleza", "America/Fortaleza"), + ("America/Glace_Bay", "America/Glace_Bay"), + ("America/Godthab", "America/Godthab"), + ("America/Goose_Bay", "America/Goose_Bay"), + ("America/Grand_Turk", "America/Grand_Turk"), + ("America/Grenada", "America/Grenada"), + ("America/Guadeloupe", "America/Guadeloupe"), + ("America/Guatemala", "America/Guatemala"), + ("America/Guayaquil", "America/Guayaquil"), + ("America/Guyana", "America/Guyana"), + ("America/Halifax", "America/Halifax"), + ("America/Havana", "America/Havana"), + ("America/Hermosillo", "America/Hermosillo"), + ( + "America/Indiana/Indianapolis", + "America/Indiana/Indianapolis", + ), + ("America/Indiana/Knox", "America/Indiana/Knox"), + ("America/Indiana/Marengo", "America/Indiana/Marengo"), + ( + "America/Indiana/Petersburg", + "America/Indiana/Petersburg", + ), + ("America/Indiana/Tell_City", "America/Indiana/Tell_City"), + ("America/Indiana/Vevay", "America/Indiana/Vevay"), + ("America/Indiana/Vincennes", "America/Indiana/Vincennes"), + ("America/Indiana/Winamac", "America/Indiana/Winamac"), + ("America/Indianapolis", "America/Indianapolis"), + ("America/Inuvik", "America/Inuvik"), + ("America/Iqaluit", "America/Iqaluit"), + ("America/Jamaica", "America/Jamaica"), + ("America/Jujuy", "America/Jujuy"), + ("America/Juneau", "America/Juneau"), + ( + "America/Kentucky/Louisville", + "America/Kentucky/Louisville", + ), + ( + "America/Kentucky/Monticello", + "America/Kentucky/Monticello", + ), + ("America/Knox_IN", "America/Knox_IN"), + ("America/Kralendijk", "America/Kralendijk"), + ("America/La_Paz", "America/La_Paz"), + ("America/Lima", "America/Lima"), + ("America/Los_Angeles", "America/Los_Angeles"), + ("America/Louisville", "America/Louisville"), + ("America/Lower_Princes", "America/Lower_Princes"), + ("America/Maceio", "America/Maceio"), + ("America/Managua", "America/Managua"), + ("America/Manaus", "America/Manaus"), + ("America/Marigot", "America/Marigot"), + ("America/Martinique", "America/Martinique"), + ("America/Matamoros", "America/Matamoros"), + ("America/Mazatlan", "America/Mazatlan"), + ("America/Mendoza", "America/Mendoza"), + ("America/Menominee", "America/Menominee"), + ("America/Merida", "America/Merida"), + ("America/Metlakatla", "America/Metlakatla"), + ("America/Mexico_City", "America/Mexico_City"), + ("America/Miquelon", "America/Miquelon"), + ("America/Moncton", "America/Moncton"), + ("America/Monterrey", "America/Monterrey"), + ("America/Montevideo", "America/Montevideo"), + ("America/Montreal", "America/Montreal"), + ("America/Montserrat", "America/Montserrat"), + ("America/Nassau", "America/Nassau"), + ("America/New_York", "America/New_York"), + ("America/Nipigon", "America/Nipigon"), + ("America/Nome", "America/Nome"), + ("America/Noronha", "America/Noronha"), + ( + "America/North_Dakota/Beulah", + "America/North_Dakota/Beulah", + ), + ( + "America/North_Dakota/Center", + "America/North_Dakota/Center", + ), + ( + "America/North_Dakota/New_Salem", + "America/North_Dakota/New_Salem", + ), + ("America/Nuuk", "America/Nuuk"), + ("America/Ojinaga", "America/Ojinaga"), + ("America/Panama", "America/Panama"), + ("America/Pangnirtung", "America/Pangnirtung"), + ("America/Paramaribo", "America/Paramaribo"), + ("America/Phoenix", "America/Phoenix"), + ("America/Port-au-Prince", "America/Port-au-Prince"), + ("America/Port_of_Spain", "America/Port_of_Spain"), + ("America/Porto_Acre", "America/Porto_Acre"), + ("America/Porto_Velho", "America/Porto_Velho"), + ("America/Puerto_Rico", "America/Puerto_Rico"), + ("America/Punta_Arenas", "America/Punta_Arenas"), + ("America/Rainy_River", "America/Rainy_River"), + ("America/Rankin_Inlet", "America/Rankin_Inlet"), + ("America/Recife", "America/Recife"), + ("America/Regina", "America/Regina"), + ("America/Resolute", "America/Resolute"), + ("America/Rio_Branco", "America/Rio_Branco"), + ("America/Rosario", "America/Rosario"), + ("America/Santa_Isabel", "America/Santa_Isabel"), + ("America/Santarem", "America/Santarem"), + ("America/Santiago", "America/Santiago"), + ("America/Santo_Domingo", "America/Santo_Domingo"), + ("America/Sao_Paulo", "America/Sao_Paulo"), + ("America/Scoresbysund", "America/Scoresbysund"), + ("America/Shiprock", "America/Shiprock"), + ("America/Sitka", "America/Sitka"), + ("America/St_Barthelemy", "America/St_Barthelemy"), + ("America/St_Johns", "America/St_Johns"), + ("America/St_Kitts", "America/St_Kitts"), + ("America/St_Lucia", "America/St_Lucia"), + ("America/St_Thomas", "America/St_Thomas"), + ("America/St_Vincent", "America/St_Vincent"), + ("America/Swift_Current", "America/Swift_Current"), + ("America/Tegucigalpa", "America/Tegucigalpa"), + ("America/Thule", "America/Thule"), + ("America/Thunder_Bay", "America/Thunder_Bay"), + ("America/Tijuana", "America/Tijuana"), + ("America/Toronto", "America/Toronto"), + ("America/Tortola", "America/Tortola"), + ("America/Vancouver", "America/Vancouver"), + ("America/Virgin", "America/Virgin"), + ("America/Whitehorse", "America/Whitehorse"), + ("America/Winnipeg", "America/Winnipeg"), + ("America/Yakutat", "America/Yakutat"), + ("America/Yellowknife", "America/Yellowknife"), + ("Antarctica/Casey", "Antarctica/Casey"), + ("Antarctica/Davis", "Antarctica/Davis"), + ("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"), + ("Antarctica/Macquarie", "Antarctica/Macquarie"), + ("Antarctica/Mawson", "Antarctica/Mawson"), + ("Antarctica/McMurdo", "Antarctica/McMurdo"), + ("Antarctica/Palmer", "Antarctica/Palmer"), + ("Antarctica/Rothera", "Antarctica/Rothera"), + ("Antarctica/South_Pole", "Antarctica/South_Pole"), + ("Antarctica/Syowa", "Antarctica/Syowa"), + ("Antarctica/Troll", "Antarctica/Troll"), + ("Antarctica/Vostok", "Antarctica/Vostok"), + ("Arctic/Longyearbyen", "Arctic/Longyearbyen"), + ("Asia/Aden", "Asia/Aden"), + ("Asia/Almaty", "Asia/Almaty"), + ("Asia/Amman", "Asia/Amman"), + ("Asia/Anadyr", "Asia/Anadyr"), + ("Asia/Aqtau", "Asia/Aqtau"), + ("Asia/Aqtobe", "Asia/Aqtobe"), + ("Asia/Ashgabat", "Asia/Ashgabat"), + ("Asia/Ashkhabad", "Asia/Ashkhabad"), + ("Asia/Atyrau", "Asia/Atyrau"), + ("Asia/Baghdad", "Asia/Baghdad"), + ("Asia/Bahrain", "Asia/Bahrain"), + ("Asia/Baku", "Asia/Baku"), + ("Asia/Bangkok", "Asia/Bangkok"), + ("Asia/Barnaul", "Asia/Barnaul"), + ("Asia/Beirut", "Asia/Beirut"), + ("Asia/Bishkek", "Asia/Bishkek"), + ("Asia/Brunei", "Asia/Brunei"), + ("Asia/Calcutta", "Asia/Calcutta"), + ("Asia/Chita", "Asia/Chita"), + ("Asia/Choibalsan", "Asia/Choibalsan"), + ("Asia/Chongqing", "Asia/Chongqing"), + ("Asia/Chungking", "Asia/Chungking"), + ("Asia/Colombo", "Asia/Colombo"), + ("Asia/Dacca", "Asia/Dacca"), + ("Asia/Damascus", "Asia/Damascus"), + ("Asia/Dhaka", "Asia/Dhaka"), + ("Asia/Dili", "Asia/Dili"), + ("Asia/Dubai", "Asia/Dubai"), + ("Asia/Dushanbe", "Asia/Dushanbe"), + ("Asia/Famagusta", "Asia/Famagusta"), + ("Asia/Gaza", "Asia/Gaza"), + ("Asia/Harbin", "Asia/Harbin"), + ("Asia/Hebron", "Asia/Hebron"), + ("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"), + ("Asia/Hong_Kong", "Asia/Hong_Kong"), + ("Asia/Hovd", "Asia/Hovd"), + ("Asia/Irkutsk", "Asia/Irkutsk"), + ("Asia/Istanbul", "Asia/Istanbul"), + ("Asia/Jakarta", "Asia/Jakarta"), + ("Asia/Jayapura", "Asia/Jayapura"), + ("Asia/Jerusalem", "Asia/Jerusalem"), + ("Asia/Kabul", "Asia/Kabul"), + ("Asia/Kamchatka", "Asia/Kamchatka"), + ("Asia/Karachi", "Asia/Karachi"), + ("Asia/Kashgar", "Asia/Kashgar"), + ("Asia/Kathmandu", "Asia/Kathmandu"), + ("Asia/Katmandu", "Asia/Katmandu"), + ("Asia/Khandyga", "Asia/Khandyga"), + ("Asia/Kolkata", "Asia/Kolkata"), + ("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"), + ("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"), + ("Asia/Kuching", "Asia/Kuching"), + ("Asia/Kuwait", "Asia/Kuwait"), + ("Asia/Macao", "Asia/Macao"), + ("Asia/Macau", "Asia/Macau"), + ("Asia/Magadan", "Asia/Magadan"), + ("Asia/Makassar", "Asia/Makassar"), + ("Asia/Manila", "Asia/Manila"), + ("Asia/Muscat", "Asia/Muscat"), + ("Asia/Nicosia", "Asia/Nicosia"), + ("Asia/Novokuznetsk", "Asia/Novokuznetsk"), + ("Asia/Novosibirsk", "Asia/Novosibirsk"), + ("Asia/Omsk", "Asia/Omsk"), + ("Asia/Oral", "Asia/Oral"), + ("Asia/Phnom_Penh", "Asia/Phnom_Penh"), + ("Asia/Pontianak", "Asia/Pontianak"), + ("Asia/Pyongyang", "Asia/Pyongyang"), + ("Asia/Qatar", "Asia/Qatar"), + ("Asia/Qostanay", "Asia/Qostanay"), + ("Asia/Qyzylorda", "Asia/Qyzylorda"), + ("Asia/Rangoon", "Asia/Rangoon"), + ("Asia/Riyadh", "Asia/Riyadh"), + ("Asia/Saigon", "Asia/Saigon"), + ("Asia/Sakhalin", "Asia/Sakhalin"), + ("Asia/Samarkand", "Asia/Samarkand"), + ("Asia/Seoul", "Asia/Seoul"), + ("Asia/Shanghai", "Asia/Shanghai"), + ("Asia/Singapore", "Asia/Singapore"), + ("Asia/Srednekolymsk", "Asia/Srednekolymsk"), + ("Asia/Taipei", "Asia/Taipei"), + ("Asia/Tashkent", "Asia/Tashkent"), + ("Asia/Tbilisi", "Asia/Tbilisi"), + ("Asia/Tehran", "Asia/Tehran"), + ("Asia/Tel_Aviv", "Asia/Tel_Aviv"), + ("Asia/Thimbu", "Asia/Thimbu"), + ("Asia/Thimphu", "Asia/Thimphu"), + ("Asia/Tokyo", "Asia/Tokyo"), + ("Asia/Tomsk", "Asia/Tomsk"), + ("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"), + ("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"), + ("Asia/Ulan_Bator", "Asia/Ulan_Bator"), + ("Asia/Urumqi", "Asia/Urumqi"), + ("Asia/Ust-Nera", "Asia/Ust-Nera"), + ("Asia/Vientiane", "Asia/Vientiane"), + ("Asia/Vladivostok", "Asia/Vladivostok"), + ("Asia/Yakutsk", "Asia/Yakutsk"), + ("Asia/Yangon", "Asia/Yangon"), + ("Asia/Yekaterinburg", "Asia/Yekaterinburg"), + ("Asia/Yerevan", "Asia/Yerevan"), + ("Atlantic/Azores", "Atlantic/Azores"), + ("Atlantic/Bermuda", "Atlantic/Bermuda"), + ("Atlantic/Canary", "Atlantic/Canary"), + ("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"), + ("Atlantic/Faeroe", "Atlantic/Faeroe"), + ("Atlantic/Faroe", "Atlantic/Faroe"), + ("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"), + ("Atlantic/Madeira", "Atlantic/Madeira"), + ("Atlantic/Reykjavik", "Atlantic/Reykjavik"), + ("Atlantic/South_Georgia", "Atlantic/South_Georgia"), + ("Atlantic/St_Helena", "Atlantic/St_Helena"), + ("Atlantic/Stanley", "Atlantic/Stanley"), + ("Australia/ACT", "Australia/ACT"), + ("Australia/Adelaide", "Australia/Adelaide"), + ("Australia/Brisbane", "Australia/Brisbane"), + ("Australia/Broken_Hill", "Australia/Broken_Hill"), + ("Australia/Canberra", "Australia/Canberra"), + ("Australia/Currie", "Australia/Currie"), + ("Australia/Darwin", "Australia/Darwin"), + ("Australia/Eucla", "Australia/Eucla"), + ("Australia/Hobart", "Australia/Hobart"), + ("Australia/LHI", "Australia/LHI"), + ("Australia/Lindeman", "Australia/Lindeman"), + ("Australia/Lord_Howe", "Australia/Lord_Howe"), + ("Australia/Melbourne", "Australia/Melbourne"), + ("Australia/NSW", "Australia/NSW"), + ("Australia/North", "Australia/North"), + ("Australia/Perth", "Australia/Perth"), + ("Australia/Queensland", "Australia/Queensland"), + ("Australia/South", "Australia/South"), + ("Australia/Sydney", "Australia/Sydney"), + ("Australia/Tasmania", "Australia/Tasmania"), + ("Australia/Victoria", "Australia/Victoria"), + ("Australia/West", "Australia/West"), + ("Australia/Yancowinna", "Australia/Yancowinna"), + ("Brazil/Acre", "Brazil/Acre"), + ("Brazil/DeNoronha", "Brazil/DeNoronha"), + ("Brazil/East", "Brazil/East"), + ("Brazil/West", "Brazil/West"), + ("CET", "CET"), + ("CST6CDT", "CST6CDT"), + ("Canada/Atlantic", "Canada/Atlantic"), + ("Canada/Central", "Canada/Central"), + ("Canada/Eastern", "Canada/Eastern"), + ("Canada/Mountain", "Canada/Mountain"), + ("Canada/Newfoundland", "Canada/Newfoundland"), + ("Canada/Pacific", "Canada/Pacific"), + ("Canada/Saskatchewan", "Canada/Saskatchewan"), + ("Canada/Yukon", "Canada/Yukon"), + ("Chile/Continental", "Chile/Continental"), + ("Chile/EasterIsland", "Chile/EasterIsland"), + ("Cuba", "Cuba"), + ("EET", "EET"), + ("EST", "EST"), + ("EST5EDT", "EST5EDT"), + ("Egypt", "Egypt"), + ("Eire", "Eire"), + ("Etc/GMT", "Etc/GMT"), + ("Etc/GMT+0", "Etc/GMT+0"), + ("Etc/GMT+1", "Etc/GMT+1"), + ("Etc/GMT+10", "Etc/GMT+10"), + ("Etc/GMT+11", "Etc/GMT+11"), + ("Etc/GMT+12", "Etc/GMT+12"), + ("Etc/GMT+2", "Etc/GMT+2"), + ("Etc/GMT+3", "Etc/GMT+3"), + ("Etc/GMT+4", "Etc/GMT+4"), + ("Etc/GMT+5", "Etc/GMT+5"), + ("Etc/GMT+6", "Etc/GMT+6"), + ("Etc/GMT+7", "Etc/GMT+7"), + ("Etc/GMT+8", "Etc/GMT+8"), + ("Etc/GMT+9", "Etc/GMT+9"), + ("Etc/GMT-0", "Etc/GMT-0"), + ("Etc/GMT-1", "Etc/GMT-1"), + ("Etc/GMT-10", "Etc/GMT-10"), + ("Etc/GMT-11", "Etc/GMT-11"), + ("Etc/GMT-12", "Etc/GMT-12"), + ("Etc/GMT-13", "Etc/GMT-13"), + ("Etc/GMT-14", "Etc/GMT-14"), + ("Etc/GMT-2", "Etc/GMT-2"), + ("Etc/GMT-3", "Etc/GMT-3"), + ("Etc/GMT-4", "Etc/GMT-4"), + ("Etc/GMT-5", "Etc/GMT-5"), + ("Etc/GMT-6", "Etc/GMT-6"), + ("Etc/GMT-7", "Etc/GMT-7"), + ("Etc/GMT-8", "Etc/GMT-8"), + ("Etc/GMT-9", "Etc/GMT-9"), + ("Etc/GMT0", "Etc/GMT0"), + ("Etc/Greenwich", "Etc/Greenwich"), + ("Etc/UCT", "Etc/UCT"), + ("Etc/UTC", "Etc/UTC"), + ("Etc/Universal", "Etc/Universal"), + ("Etc/Zulu", "Etc/Zulu"), + ("Europe/Amsterdam", "Europe/Amsterdam"), + ("Europe/Andorra", "Europe/Andorra"), + ("Europe/Astrakhan", "Europe/Astrakhan"), + ("Europe/Athens", "Europe/Athens"), + ("Europe/Belfast", "Europe/Belfast"), + ("Europe/Belgrade", "Europe/Belgrade"), + ("Europe/Berlin", "Europe/Berlin"), + ("Europe/Bratislava", "Europe/Bratislava"), + ("Europe/Brussels", "Europe/Brussels"), + ("Europe/Bucharest", "Europe/Bucharest"), + ("Europe/Budapest", "Europe/Budapest"), + ("Europe/Busingen", "Europe/Busingen"), + ("Europe/Chisinau", "Europe/Chisinau"), + ("Europe/Copenhagen", "Europe/Copenhagen"), + ("Europe/Dublin", "Europe/Dublin"), + ("Europe/Gibraltar", "Europe/Gibraltar"), + ("Europe/Guernsey", "Europe/Guernsey"), + ("Europe/Helsinki", "Europe/Helsinki"), + ("Europe/Isle_of_Man", "Europe/Isle_of_Man"), + ("Europe/Istanbul", "Europe/Istanbul"), + ("Europe/Jersey", "Europe/Jersey"), + ("Europe/Kaliningrad", "Europe/Kaliningrad"), + ("Europe/Kiev", "Europe/Kiev"), + ("Europe/Kirov", "Europe/Kirov"), + ("Europe/Kyiv", "Europe/Kyiv"), + ("Europe/Lisbon", "Europe/Lisbon"), + ("Europe/Ljubljana", "Europe/Ljubljana"), + ("Europe/London", "Europe/London"), + ("Europe/Luxembourg", "Europe/Luxembourg"), + ("Europe/Madrid", "Europe/Madrid"), + ("Europe/Malta", "Europe/Malta"), + ("Europe/Mariehamn", "Europe/Mariehamn"), + ("Europe/Minsk", "Europe/Minsk"), + ("Europe/Monaco", "Europe/Monaco"), + ("Europe/Moscow", "Europe/Moscow"), + ("Europe/Nicosia", "Europe/Nicosia"), + ("Europe/Oslo", "Europe/Oslo"), + ("Europe/Paris", "Europe/Paris"), + ("Europe/Podgorica", "Europe/Podgorica"), + ("Europe/Prague", "Europe/Prague"), + ("Europe/Riga", "Europe/Riga"), + ("Europe/Rome", "Europe/Rome"), + ("Europe/Samara", "Europe/Samara"), + ("Europe/San_Marino", "Europe/San_Marino"), + ("Europe/Sarajevo", "Europe/Sarajevo"), + ("Europe/Saratov", "Europe/Saratov"), + ("Europe/Simferopol", "Europe/Simferopol"), + ("Europe/Skopje", "Europe/Skopje"), + ("Europe/Sofia", "Europe/Sofia"), + ("Europe/Stockholm", "Europe/Stockholm"), + ("Europe/Tallinn", "Europe/Tallinn"), + ("Europe/Tirane", "Europe/Tirane"), + ("Europe/Tiraspol", "Europe/Tiraspol"), + ("Europe/Ulyanovsk", "Europe/Ulyanovsk"), + ("Europe/Uzhgorod", "Europe/Uzhgorod"), + ("Europe/Vaduz", "Europe/Vaduz"), + ("Europe/Vatican", "Europe/Vatican"), + ("Europe/Vienna", "Europe/Vienna"), + ("Europe/Vilnius", "Europe/Vilnius"), + ("Europe/Volgograd", "Europe/Volgograd"), + ("Europe/Warsaw", "Europe/Warsaw"), + ("Europe/Zagreb", "Europe/Zagreb"), + ("Europe/Zaporozhye", "Europe/Zaporozhye"), + ("Europe/Zurich", "Europe/Zurich"), + ("GB", "GB"), + ("GB-Eire", "GB-Eire"), + ("GMT", "GMT"), + ("GMT+0", "GMT+0"), + ("GMT-0", "GMT-0"), + ("GMT0", "GMT0"), + ("Greenwich", "Greenwich"), + ("HST", "HST"), + ("Hongkong", "Hongkong"), + ("Iceland", "Iceland"), + ("Indian/Antananarivo", "Indian/Antananarivo"), + ("Indian/Chagos", "Indian/Chagos"), + ("Indian/Christmas", "Indian/Christmas"), + ("Indian/Cocos", "Indian/Cocos"), + ("Indian/Comoro", "Indian/Comoro"), + ("Indian/Kerguelen", "Indian/Kerguelen"), + ("Indian/Mahe", "Indian/Mahe"), + ("Indian/Maldives", "Indian/Maldives"), + ("Indian/Mauritius", "Indian/Mauritius"), + ("Indian/Mayotte", "Indian/Mayotte"), + ("Indian/Reunion", "Indian/Reunion"), + ("Iran", "Iran"), + ("Israel", "Israel"), + ("Jamaica", "Jamaica"), + ("Japan", "Japan"), + ("Kwajalein", "Kwajalein"), + ("Libya", "Libya"), + ("MET", "MET"), + ("MST", "MST"), + ("MST7MDT", "MST7MDT"), + ("Mexico/BajaNorte", "Mexico/BajaNorte"), + ("Mexico/BajaSur", "Mexico/BajaSur"), + ("Mexico/General", "Mexico/General"), + ("NZ", "NZ"), + ("NZ-CHAT", "NZ-CHAT"), + ("Navajo", "Navajo"), + ("PRC", "PRC"), + ("PST8PDT", "PST8PDT"), + ("Pacific/Apia", "Pacific/Apia"), + ("Pacific/Auckland", "Pacific/Auckland"), + ("Pacific/Bougainville", "Pacific/Bougainville"), + ("Pacific/Chatham", "Pacific/Chatham"), + ("Pacific/Chuuk", "Pacific/Chuuk"), + ("Pacific/Easter", "Pacific/Easter"), + ("Pacific/Efate", "Pacific/Efate"), + ("Pacific/Enderbury", "Pacific/Enderbury"), + ("Pacific/Fakaofo", "Pacific/Fakaofo"), + ("Pacific/Fiji", "Pacific/Fiji"), + ("Pacific/Funafuti", "Pacific/Funafuti"), + ("Pacific/Galapagos", "Pacific/Galapagos"), + ("Pacific/Gambier", "Pacific/Gambier"), + ("Pacific/Guadalcanal", "Pacific/Guadalcanal"), + ("Pacific/Guam", "Pacific/Guam"), + ("Pacific/Honolulu", "Pacific/Honolulu"), + ("Pacific/Johnston", "Pacific/Johnston"), + ("Pacific/Kanton", "Pacific/Kanton"), + ("Pacific/Kiritimati", "Pacific/Kiritimati"), + ("Pacific/Kosrae", "Pacific/Kosrae"), + ("Pacific/Kwajalein", "Pacific/Kwajalein"), + ("Pacific/Majuro", "Pacific/Majuro"), + ("Pacific/Marquesas", "Pacific/Marquesas"), + ("Pacific/Midway", "Pacific/Midway"), + ("Pacific/Nauru", "Pacific/Nauru"), + ("Pacific/Niue", "Pacific/Niue"), + ("Pacific/Norfolk", "Pacific/Norfolk"), + ("Pacific/Noumea", "Pacific/Noumea"), + ("Pacific/Pago_Pago", "Pacific/Pago_Pago"), + ("Pacific/Palau", "Pacific/Palau"), + ("Pacific/Pitcairn", "Pacific/Pitcairn"), + ("Pacific/Pohnpei", "Pacific/Pohnpei"), + ("Pacific/Ponape", "Pacific/Ponape"), + ("Pacific/Port_Moresby", "Pacific/Port_Moresby"), + ("Pacific/Rarotonga", "Pacific/Rarotonga"), + ("Pacific/Saipan", "Pacific/Saipan"), + ("Pacific/Samoa", "Pacific/Samoa"), + ("Pacific/Tahiti", "Pacific/Tahiti"), + ("Pacific/Tarawa", "Pacific/Tarawa"), + ("Pacific/Tongatapu", "Pacific/Tongatapu"), + ("Pacific/Truk", "Pacific/Truk"), + ("Pacific/Wake", "Pacific/Wake"), + ("Pacific/Wallis", "Pacific/Wallis"), + ("Pacific/Yap", "Pacific/Yap"), + ("Poland", "Poland"), + ("Portugal", "Portugal"), + ("ROC", "ROC"), + ("ROK", "ROK"), + ("Singapore", "Singapore"), + ("Turkey", "Turkey"), + ("UCT", "UCT"), + ("US/Alaska", "US/Alaska"), + ("US/Aleutian", "US/Aleutian"), + ("US/Arizona", "US/Arizona"), + ("US/Central", "US/Central"), + ("US/East-Indiana", "US/East-Indiana"), + ("US/Eastern", "US/Eastern"), + ("US/Hawaii", "US/Hawaii"), + ("US/Indiana-Starke", "US/Indiana-Starke"), + ("US/Michigan", "US/Michigan"), + ("US/Mountain", "US/Mountain"), + ("US/Pacific", "US/Pacific"), + ("US/Samoa", "US/Samoa"), + ("UTC", "UTC"), + ("Universal", "Universal"), + ("W-SU", "W-SU"), + ("WET", "WET"), + ("Zulu", "Zulu"), + ], + default="UTC", + max_length=255, + ), + ), + migrations.AddField( + model_name="project", + name="timezone", + field=models.CharField( + choices=[ + ("Africa/Abidjan", "Africa/Abidjan"), + ("Africa/Accra", "Africa/Accra"), + ("Africa/Addis_Ababa", "Africa/Addis_Ababa"), + ("Africa/Algiers", "Africa/Algiers"), + ("Africa/Asmara", "Africa/Asmara"), + ("Africa/Asmera", "Africa/Asmera"), + ("Africa/Bamako", "Africa/Bamako"), + ("Africa/Bangui", "Africa/Bangui"), + ("Africa/Banjul", "Africa/Banjul"), + ("Africa/Bissau", "Africa/Bissau"), + ("Africa/Blantyre", "Africa/Blantyre"), + ("Africa/Brazzaville", "Africa/Brazzaville"), + ("Africa/Bujumbura", "Africa/Bujumbura"), + ("Africa/Cairo", "Africa/Cairo"), + ("Africa/Casablanca", "Africa/Casablanca"), + ("Africa/Ceuta", "Africa/Ceuta"), + ("Africa/Conakry", "Africa/Conakry"), + ("Africa/Dakar", "Africa/Dakar"), + ("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"), + ("Africa/Djibouti", "Africa/Djibouti"), + ("Africa/Douala", "Africa/Douala"), + ("Africa/El_Aaiun", "Africa/El_Aaiun"), + ("Africa/Freetown", "Africa/Freetown"), + ("Africa/Gaborone", "Africa/Gaborone"), + ("Africa/Harare", "Africa/Harare"), + ("Africa/Johannesburg", "Africa/Johannesburg"), + ("Africa/Juba", "Africa/Juba"), + ("Africa/Kampala", "Africa/Kampala"), + ("Africa/Khartoum", "Africa/Khartoum"), + ("Africa/Kigali", "Africa/Kigali"), + ("Africa/Kinshasa", "Africa/Kinshasa"), + ("Africa/Lagos", "Africa/Lagos"), + ("Africa/Libreville", "Africa/Libreville"), + ("Africa/Lome", "Africa/Lome"), + ("Africa/Luanda", "Africa/Luanda"), + ("Africa/Lubumbashi", "Africa/Lubumbashi"), + ("Africa/Lusaka", "Africa/Lusaka"), + ("Africa/Malabo", "Africa/Malabo"), + ("Africa/Maputo", "Africa/Maputo"), + ("Africa/Maseru", "Africa/Maseru"), + ("Africa/Mbabane", "Africa/Mbabane"), + ("Africa/Mogadishu", "Africa/Mogadishu"), + ("Africa/Monrovia", "Africa/Monrovia"), + ("Africa/Nairobi", "Africa/Nairobi"), + ("Africa/Ndjamena", "Africa/Ndjamena"), + ("Africa/Niamey", "Africa/Niamey"), + ("Africa/Nouakchott", "Africa/Nouakchott"), + ("Africa/Ouagadougou", "Africa/Ouagadougou"), + ("Africa/Porto-Novo", "Africa/Porto-Novo"), + ("Africa/Sao_Tome", "Africa/Sao_Tome"), + ("Africa/Timbuktu", "Africa/Timbuktu"), + ("Africa/Tripoli", "Africa/Tripoli"), + ("Africa/Tunis", "Africa/Tunis"), + ("Africa/Windhoek", "Africa/Windhoek"), + ("America/Adak", "America/Adak"), + ("America/Anchorage", "America/Anchorage"), + ("America/Anguilla", "America/Anguilla"), + ("America/Antigua", "America/Antigua"), + ("America/Araguaina", "America/Araguaina"), + ( + "America/Argentina/Buenos_Aires", + "America/Argentina/Buenos_Aires", + ), + ( + "America/Argentina/Catamarca", + "America/Argentina/Catamarca", + ), + ( + "America/Argentina/ComodRivadavia", + "America/Argentina/ComodRivadavia", + ), + ("America/Argentina/Cordoba", "America/Argentina/Cordoba"), + ("America/Argentina/Jujuy", "America/Argentina/Jujuy"), + ( + "America/Argentina/La_Rioja", + "America/Argentina/La_Rioja", + ), + ("America/Argentina/Mendoza", "America/Argentina/Mendoza"), + ( + "America/Argentina/Rio_Gallegos", + "America/Argentina/Rio_Gallegos", + ), + ("America/Argentina/Salta", "America/Argentina/Salta"), + ( + "America/Argentina/San_Juan", + "America/Argentina/San_Juan", + ), + ( + "America/Argentina/San_Luis", + "America/Argentina/San_Luis", + ), + ("America/Argentina/Tucuman", "America/Argentina/Tucuman"), + ("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"), + ("America/Aruba", "America/Aruba"), + ("America/Asuncion", "America/Asuncion"), + ("America/Atikokan", "America/Atikokan"), + ("America/Atka", "America/Atka"), + ("America/Bahia", "America/Bahia"), + ("America/Bahia_Banderas", "America/Bahia_Banderas"), + ("America/Barbados", "America/Barbados"), + ("America/Belem", "America/Belem"), + ("America/Belize", "America/Belize"), + ("America/Blanc-Sablon", "America/Blanc-Sablon"), + ("America/Boa_Vista", "America/Boa_Vista"), + ("America/Bogota", "America/Bogota"), + ("America/Boise", "America/Boise"), + ("America/Buenos_Aires", "America/Buenos_Aires"), + ("America/Cambridge_Bay", "America/Cambridge_Bay"), + ("America/Campo_Grande", "America/Campo_Grande"), + ("America/Cancun", "America/Cancun"), + ("America/Caracas", "America/Caracas"), + ("America/Catamarca", "America/Catamarca"), + ("America/Cayenne", "America/Cayenne"), + ("America/Cayman", "America/Cayman"), + ("America/Chicago", "America/Chicago"), + ("America/Chihuahua", "America/Chihuahua"), + ("America/Ciudad_Juarez", "America/Ciudad_Juarez"), + ("America/Coral_Harbour", "America/Coral_Harbour"), + ("America/Cordoba", "America/Cordoba"), + ("America/Costa_Rica", "America/Costa_Rica"), + ("America/Creston", "America/Creston"), + ("America/Cuiaba", "America/Cuiaba"), + ("America/Curacao", "America/Curacao"), + ("America/Danmarkshavn", "America/Danmarkshavn"), + ("America/Dawson", "America/Dawson"), + ("America/Dawson_Creek", "America/Dawson_Creek"), + ("America/Denver", "America/Denver"), + ("America/Detroit", "America/Detroit"), + ("America/Dominica", "America/Dominica"), + ("America/Edmonton", "America/Edmonton"), + ("America/Eirunepe", "America/Eirunepe"), + ("America/El_Salvador", "America/El_Salvador"), + ("America/Ensenada", "America/Ensenada"), + ("America/Fort_Nelson", "America/Fort_Nelson"), + ("America/Fort_Wayne", "America/Fort_Wayne"), + ("America/Fortaleza", "America/Fortaleza"), + ("America/Glace_Bay", "America/Glace_Bay"), + ("America/Godthab", "America/Godthab"), + ("America/Goose_Bay", "America/Goose_Bay"), + ("America/Grand_Turk", "America/Grand_Turk"), + ("America/Grenada", "America/Grenada"), + ("America/Guadeloupe", "America/Guadeloupe"), + ("America/Guatemala", "America/Guatemala"), + ("America/Guayaquil", "America/Guayaquil"), + ("America/Guyana", "America/Guyana"), + ("America/Halifax", "America/Halifax"), + ("America/Havana", "America/Havana"), + ("America/Hermosillo", "America/Hermosillo"), + ( + "America/Indiana/Indianapolis", + "America/Indiana/Indianapolis", + ), + ("America/Indiana/Knox", "America/Indiana/Knox"), + ("America/Indiana/Marengo", "America/Indiana/Marengo"), + ( + "America/Indiana/Petersburg", + "America/Indiana/Petersburg", + ), + ("America/Indiana/Tell_City", "America/Indiana/Tell_City"), + ("America/Indiana/Vevay", "America/Indiana/Vevay"), + ("America/Indiana/Vincennes", "America/Indiana/Vincennes"), + ("America/Indiana/Winamac", "America/Indiana/Winamac"), + ("America/Indianapolis", "America/Indianapolis"), + ("America/Inuvik", "America/Inuvik"), + ("America/Iqaluit", "America/Iqaluit"), + ("America/Jamaica", "America/Jamaica"), + ("America/Jujuy", "America/Jujuy"), + ("America/Juneau", "America/Juneau"), + ( + "America/Kentucky/Louisville", + "America/Kentucky/Louisville", + ), + ( + "America/Kentucky/Monticello", + "America/Kentucky/Monticello", + ), + ("America/Knox_IN", "America/Knox_IN"), + ("America/Kralendijk", "America/Kralendijk"), + ("America/La_Paz", "America/La_Paz"), + ("America/Lima", "America/Lima"), + ("America/Los_Angeles", "America/Los_Angeles"), + ("America/Louisville", "America/Louisville"), + ("America/Lower_Princes", "America/Lower_Princes"), + ("America/Maceio", "America/Maceio"), + ("America/Managua", "America/Managua"), + ("America/Manaus", "America/Manaus"), + ("America/Marigot", "America/Marigot"), + ("America/Martinique", "America/Martinique"), + ("America/Matamoros", "America/Matamoros"), + ("America/Mazatlan", "America/Mazatlan"), + ("America/Mendoza", "America/Mendoza"), + ("America/Menominee", "America/Menominee"), + ("America/Merida", "America/Merida"), + ("America/Metlakatla", "America/Metlakatla"), + ("America/Mexico_City", "America/Mexico_City"), + ("America/Miquelon", "America/Miquelon"), + ("America/Moncton", "America/Moncton"), + ("America/Monterrey", "America/Monterrey"), + ("America/Montevideo", "America/Montevideo"), + ("America/Montreal", "America/Montreal"), + ("America/Montserrat", "America/Montserrat"), + ("America/Nassau", "America/Nassau"), + ("America/New_York", "America/New_York"), + ("America/Nipigon", "America/Nipigon"), + ("America/Nome", "America/Nome"), + ("America/Noronha", "America/Noronha"), + ( + "America/North_Dakota/Beulah", + "America/North_Dakota/Beulah", + ), + ( + "America/North_Dakota/Center", + "America/North_Dakota/Center", + ), + ( + "America/North_Dakota/New_Salem", + "America/North_Dakota/New_Salem", + ), + ("America/Nuuk", "America/Nuuk"), + ("America/Ojinaga", "America/Ojinaga"), + ("America/Panama", "America/Panama"), + ("America/Pangnirtung", "America/Pangnirtung"), + ("America/Paramaribo", "America/Paramaribo"), + ("America/Phoenix", "America/Phoenix"), + ("America/Port-au-Prince", "America/Port-au-Prince"), + ("America/Port_of_Spain", "America/Port_of_Spain"), + ("America/Porto_Acre", "America/Porto_Acre"), + ("America/Porto_Velho", "America/Porto_Velho"), + ("America/Puerto_Rico", "America/Puerto_Rico"), + ("America/Punta_Arenas", "America/Punta_Arenas"), + ("America/Rainy_River", "America/Rainy_River"), + ("America/Rankin_Inlet", "America/Rankin_Inlet"), + ("America/Recife", "America/Recife"), + ("America/Regina", "America/Regina"), + ("America/Resolute", "America/Resolute"), + ("America/Rio_Branco", "America/Rio_Branco"), + ("America/Rosario", "America/Rosario"), + ("America/Santa_Isabel", "America/Santa_Isabel"), + ("America/Santarem", "America/Santarem"), + ("America/Santiago", "America/Santiago"), + ("America/Santo_Domingo", "America/Santo_Domingo"), + ("America/Sao_Paulo", "America/Sao_Paulo"), + ("America/Scoresbysund", "America/Scoresbysund"), + ("America/Shiprock", "America/Shiprock"), + ("America/Sitka", "America/Sitka"), + ("America/St_Barthelemy", "America/St_Barthelemy"), + ("America/St_Johns", "America/St_Johns"), + ("America/St_Kitts", "America/St_Kitts"), + ("America/St_Lucia", "America/St_Lucia"), + ("America/St_Thomas", "America/St_Thomas"), + ("America/St_Vincent", "America/St_Vincent"), + ("America/Swift_Current", "America/Swift_Current"), + ("America/Tegucigalpa", "America/Tegucigalpa"), + ("America/Thule", "America/Thule"), + ("America/Thunder_Bay", "America/Thunder_Bay"), + ("America/Tijuana", "America/Tijuana"), + ("America/Toronto", "America/Toronto"), + ("America/Tortola", "America/Tortola"), + ("America/Vancouver", "America/Vancouver"), + ("America/Virgin", "America/Virgin"), + ("America/Whitehorse", "America/Whitehorse"), + ("America/Winnipeg", "America/Winnipeg"), + ("America/Yakutat", "America/Yakutat"), + ("America/Yellowknife", "America/Yellowknife"), + ("Antarctica/Casey", "Antarctica/Casey"), + ("Antarctica/Davis", "Antarctica/Davis"), + ("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"), + ("Antarctica/Macquarie", "Antarctica/Macquarie"), + ("Antarctica/Mawson", "Antarctica/Mawson"), + ("Antarctica/McMurdo", "Antarctica/McMurdo"), + ("Antarctica/Palmer", "Antarctica/Palmer"), + ("Antarctica/Rothera", "Antarctica/Rothera"), + ("Antarctica/South_Pole", "Antarctica/South_Pole"), + ("Antarctica/Syowa", "Antarctica/Syowa"), + ("Antarctica/Troll", "Antarctica/Troll"), + ("Antarctica/Vostok", "Antarctica/Vostok"), + ("Arctic/Longyearbyen", "Arctic/Longyearbyen"), + ("Asia/Aden", "Asia/Aden"), + ("Asia/Almaty", "Asia/Almaty"), + ("Asia/Amman", "Asia/Amman"), + ("Asia/Anadyr", "Asia/Anadyr"), + ("Asia/Aqtau", "Asia/Aqtau"), + ("Asia/Aqtobe", "Asia/Aqtobe"), + ("Asia/Ashgabat", "Asia/Ashgabat"), + ("Asia/Ashkhabad", "Asia/Ashkhabad"), + ("Asia/Atyrau", "Asia/Atyrau"), + ("Asia/Baghdad", "Asia/Baghdad"), + ("Asia/Bahrain", "Asia/Bahrain"), + ("Asia/Baku", "Asia/Baku"), + ("Asia/Bangkok", "Asia/Bangkok"), + ("Asia/Barnaul", "Asia/Barnaul"), + ("Asia/Beirut", "Asia/Beirut"), + ("Asia/Bishkek", "Asia/Bishkek"), + ("Asia/Brunei", "Asia/Brunei"), + ("Asia/Calcutta", "Asia/Calcutta"), + ("Asia/Chita", "Asia/Chita"), + ("Asia/Choibalsan", "Asia/Choibalsan"), + ("Asia/Chongqing", "Asia/Chongqing"), + ("Asia/Chungking", "Asia/Chungking"), + ("Asia/Colombo", "Asia/Colombo"), + ("Asia/Dacca", "Asia/Dacca"), + ("Asia/Damascus", "Asia/Damascus"), + ("Asia/Dhaka", "Asia/Dhaka"), + ("Asia/Dili", "Asia/Dili"), + ("Asia/Dubai", "Asia/Dubai"), + ("Asia/Dushanbe", "Asia/Dushanbe"), + ("Asia/Famagusta", "Asia/Famagusta"), + ("Asia/Gaza", "Asia/Gaza"), + ("Asia/Harbin", "Asia/Harbin"), + ("Asia/Hebron", "Asia/Hebron"), + ("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"), + ("Asia/Hong_Kong", "Asia/Hong_Kong"), + ("Asia/Hovd", "Asia/Hovd"), + ("Asia/Irkutsk", "Asia/Irkutsk"), + ("Asia/Istanbul", "Asia/Istanbul"), + ("Asia/Jakarta", "Asia/Jakarta"), + ("Asia/Jayapura", "Asia/Jayapura"), + ("Asia/Jerusalem", "Asia/Jerusalem"), + ("Asia/Kabul", "Asia/Kabul"), + ("Asia/Kamchatka", "Asia/Kamchatka"), + ("Asia/Karachi", "Asia/Karachi"), + ("Asia/Kashgar", "Asia/Kashgar"), + ("Asia/Kathmandu", "Asia/Kathmandu"), + ("Asia/Katmandu", "Asia/Katmandu"), + ("Asia/Khandyga", "Asia/Khandyga"), + ("Asia/Kolkata", "Asia/Kolkata"), + ("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"), + ("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"), + ("Asia/Kuching", "Asia/Kuching"), + ("Asia/Kuwait", "Asia/Kuwait"), + ("Asia/Macao", "Asia/Macao"), + ("Asia/Macau", "Asia/Macau"), + ("Asia/Magadan", "Asia/Magadan"), + ("Asia/Makassar", "Asia/Makassar"), + ("Asia/Manila", "Asia/Manila"), + ("Asia/Muscat", "Asia/Muscat"), + ("Asia/Nicosia", "Asia/Nicosia"), + ("Asia/Novokuznetsk", "Asia/Novokuznetsk"), + ("Asia/Novosibirsk", "Asia/Novosibirsk"), + ("Asia/Omsk", "Asia/Omsk"), + ("Asia/Oral", "Asia/Oral"), + ("Asia/Phnom_Penh", "Asia/Phnom_Penh"), + ("Asia/Pontianak", "Asia/Pontianak"), + ("Asia/Pyongyang", "Asia/Pyongyang"), + ("Asia/Qatar", "Asia/Qatar"), + ("Asia/Qostanay", "Asia/Qostanay"), + ("Asia/Qyzylorda", "Asia/Qyzylorda"), + ("Asia/Rangoon", "Asia/Rangoon"), + ("Asia/Riyadh", "Asia/Riyadh"), + ("Asia/Saigon", "Asia/Saigon"), + ("Asia/Sakhalin", "Asia/Sakhalin"), + ("Asia/Samarkand", "Asia/Samarkand"), + ("Asia/Seoul", "Asia/Seoul"), + ("Asia/Shanghai", "Asia/Shanghai"), + ("Asia/Singapore", "Asia/Singapore"), + ("Asia/Srednekolymsk", "Asia/Srednekolymsk"), + ("Asia/Taipei", "Asia/Taipei"), + ("Asia/Tashkent", "Asia/Tashkent"), + ("Asia/Tbilisi", "Asia/Tbilisi"), + ("Asia/Tehran", "Asia/Tehran"), + ("Asia/Tel_Aviv", "Asia/Tel_Aviv"), + ("Asia/Thimbu", "Asia/Thimbu"), + ("Asia/Thimphu", "Asia/Thimphu"), + ("Asia/Tokyo", "Asia/Tokyo"), + ("Asia/Tomsk", "Asia/Tomsk"), + ("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"), + ("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"), + ("Asia/Ulan_Bator", "Asia/Ulan_Bator"), + ("Asia/Urumqi", "Asia/Urumqi"), + ("Asia/Ust-Nera", "Asia/Ust-Nera"), + ("Asia/Vientiane", "Asia/Vientiane"), + ("Asia/Vladivostok", "Asia/Vladivostok"), + ("Asia/Yakutsk", "Asia/Yakutsk"), + ("Asia/Yangon", "Asia/Yangon"), + ("Asia/Yekaterinburg", "Asia/Yekaterinburg"), + ("Asia/Yerevan", "Asia/Yerevan"), + ("Atlantic/Azores", "Atlantic/Azores"), + ("Atlantic/Bermuda", "Atlantic/Bermuda"), + ("Atlantic/Canary", "Atlantic/Canary"), + ("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"), + ("Atlantic/Faeroe", "Atlantic/Faeroe"), + ("Atlantic/Faroe", "Atlantic/Faroe"), + ("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"), + ("Atlantic/Madeira", "Atlantic/Madeira"), + ("Atlantic/Reykjavik", "Atlantic/Reykjavik"), + ("Atlantic/South_Georgia", "Atlantic/South_Georgia"), + ("Atlantic/St_Helena", "Atlantic/St_Helena"), + ("Atlantic/Stanley", "Atlantic/Stanley"), + ("Australia/ACT", "Australia/ACT"), + ("Australia/Adelaide", "Australia/Adelaide"), + ("Australia/Brisbane", "Australia/Brisbane"), + ("Australia/Broken_Hill", "Australia/Broken_Hill"), + ("Australia/Canberra", "Australia/Canberra"), + ("Australia/Currie", "Australia/Currie"), + ("Australia/Darwin", "Australia/Darwin"), + ("Australia/Eucla", "Australia/Eucla"), + ("Australia/Hobart", "Australia/Hobart"), + ("Australia/LHI", "Australia/LHI"), + ("Australia/Lindeman", "Australia/Lindeman"), + ("Australia/Lord_Howe", "Australia/Lord_Howe"), + ("Australia/Melbourne", "Australia/Melbourne"), + ("Australia/NSW", "Australia/NSW"), + ("Australia/North", "Australia/North"), + ("Australia/Perth", "Australia/Perth"), + ("Australia/Queensland", "Australia/Queensland"), + ("Australia/South", "Australia/South"), + ("Australia/Sydney", "Australia/Sydney"), + ("Australia/Tasmania", "Australia/Tasmania"), + ("Australia/Victoria", "Australia/Victoria"), + ("Australia/West", "Australia/West"), + ("Australia/Yancowinna", "Australia/Yancowinna"), + ("Brazil/Acre", "Brazil/Acre"), + ("Brazil/DeNoronha", "Brazil/DeNoronha"), + ("Brazil/East", "Brazil/East"), + ("Brazil/West", "Brazil/West"), + ("CET", "CET"), + ("CST6CDT", "CST6CDT"), + ("Canada/Atlantic", "Canada/Atlantic"), + ("Canada/Central", "Canada/Central"), + ("Canada/Eastern", "Canada/Eastern"), + ("Canada/Mountain", "Canada/Mountain"), + ("Canada/Newfoundland", "Canada/Newfoundland"), + ("Canada/Pacific", "Canada/Pacific"), + ("Canada/Saskatchewan", "Canada/Saskatchewan"), + ("Canada/Yukon", "Canada/Yukon"), + ("Chile/Continental", "Chile/Continental"), + ("Chile/EasterIsland", "Chile/EasterIsland"), + ("Cuba", "Cuba"), + ("EET", "EET"), + ("EST", "EST"), + ("EST5EDT", "EST5EDT"), + ("Egypt", "Egypt"), + ("Eire", "Eire"), + ("Etc/GMT", "Etc/GMT"), + ("Etc/GMT+0", "Etc/GMT+0"), + ("Etc/GMT+1", "Etc/GMT+1"), + ("Etc/GMT+10", "Etc/GMT+10"), + ("Etc/GMT+11", "Etc/GMT+11"), + ("Etc/GMT+12", "Etc/GMT+12"), + ("Etc/GMT+2", "Etc/GMT+2"), + ("Etc/GMT+3", "Etc/GMT+3"), + ("Etc/GMT+4", "Etc/GMT+4"), + ("Etc/GMT+5", "Etc/GMT+5"), + ("Etc/GMT+6", "Etc/GMT+6"), + ("Etc/GMT+7", "Etc/GMT+7"), + ("Etc/GMT+8", "Etc/GMT+8"), + ("Etc/GMT+9", "Etc/GMT+9"), + ("Etc/GMT-0", "Etc/GMT-0"), + ("Etc/GMT-1", "Etc/GMT-1"), + ("Etc/GMT-10", "Etc/GMT-10"), + ("Etc/GMT-11", "Etc/GMT-11"), + ("Etc/GMT-12", "Etc/GMT-12"), + ("Etc/GMT-13", "Etc/GMT-13"), + ("Etc/GMT-14", "Etc/GMT-14"), + ("Etc/GMT-2", "Etc/GMT-2"), + ("Etc/GMT-3", "Etc/GMT-3"), + ("Etc/GMT-4", "Etc/GMT-4"), + ("Etc/GMT-5", "Etc/GMT-5"), + ("Etc/GMT-6", "Etc/GMT-6"), + ("Etc/GMT-7", "Etc/GMT-7"), + ("Etc/GMT-8", "Etc/GMT-8"), + ("Etc/GMT-9", "Etc/GMT-9"), + ("Etc/GMT0", "Etc/GMT0"), + ("Etc/Greenwich", "Etc/Greenwich"), + ("Etc/UCT", "Etc/UCT"), + ("Etc/UTC", "Etc/UTC"), + ("Etc/Universal", "Etc/Universal"), + ("Etc/Zulu", "Etc/Zulu"), + ("Europe/Amsterdam", "Europe/Amsterdam"), + ("Europe/Andorra", "Europe/Andorra"), + ("Europe/Astrakhan", "Europe/Astrakhan"), + ("Europe/Athens", "Europe/Athens"), + ("Europe/Belfast", "Europe/Belfast"), + ("Europe/Belgrade", "Europe/Belgrade"), + ("Europe/Berlin", "Europe/Berlin"), + ("Europe/Bratislava", "Europe/Bratislava"), + ("Europe/Brussels", "Europe/Brussels"), + ("Europe/Bucharest", "Europe/Bucharest"), + ("Europe/Budapest", "Europe/Budapest"), + ("Europe/Busingen", "Europe/Busingen"), + ("Europe/Chisinau", "Europe/Chisinau"), + ("Europe/Copenhagen", "Europe/Copenhagen"), + ("Europe/Dublin", "Europe/Dublin"), + ("Europe/Gibraltar", "Europe/Gibraltar"), + ("Europe/Guernsey", "Europe/Guernsey"), + ("Europe/Helsinki", "Europe/Helsinki"), + ("Europe/Isle_of_Man", "Europe/Isle_of_Man"), + ("Europe/Istanbul", "Europe/Istanbul"), + ("Europe/Jersey", "Europe/Jersey"), + ("Europe/Kaliningrad", "Europe/Kaliningrad"), + ("Europe/Kiev", "Europe/Kiev"), + ("Europe/Kirov", "Europe/Kirov"), + ("Europe/Kyiv", "Europe/Kyiv"), + ("Europe/Lisbon", "Europe/Lisbon"), + ("Europe/Ljubljana", "Europe/Ljubljana"), + ("Europe/London", "Europe/London"), + ("Europe/Luxembourg", "Europe/Luxembourg"), + ("Europe/Madrid", "Europe/Madrid"), + ("Europe/Malta", "Europe/Malta"), + ("Europe/Mariehamn", "Europe/Mariehamn"), + ("Europe/Minsk", "Europe/Minsk"), + ("Europe/Monaco", "Europe/Monaco"), + ("Europe/Moscow", "Europe/Moscow"), + ("Europe/Nicosia", "Europe/Nicosia"), + ("Europe/Oslo", "Europe/Oslo"), + ("Europe/Paris", "Europe/Paris"), + ("Europe/Podgorica", "Europe/Podgorica"), + ("Europe/Prague", "Europe/Prague"), + ("Europe/Riga", "Europe/Riga"), + ("Europe/Rome", "Europe/Rome"), + ("Europe/Samara", "Europe/Samara"), + ("Europe/San_Marino", "Europe/San_Marino"), + ("Europe/Sarajevo", "Europe/Sarajevo"), + ("Europe/Saratov", "Europe/Saratov"), + ("Europe/Simferopol", "Europe/Simferopol"), + ("Europe/Skopje", "Europe/Skopje"), + ("Europe/Sofia", "Europe/Sofia"), + ("Europe/Stockholm", "Europe/Stockholm"), + ("Europe/Tallinn", "Europe/Tallinn"), + ("Europe/Tirane", "Europe/Tirane"), + ("Europe/Tiraspol", "Europe/Tiraspol"), + ("Europe/Ulyanovsk", "Europe/Ulyanovsk"), + ("Europe/Uzhgorod", "Europe/Uzhgorod"), + ("Europe/Vaduz", "Europe/Vaduz"), + ("Europe/Vatican", "Europe/Vatican"), + ("Europe/Vienna", "Europe/Vienna"), + ("Europe/Vilnius", "Europe/Vilnius"), + ("Europe/Volgograd", "Europe/Volgograd"), + ("Europe/Warsaw", "Europe/Warsaw"), + ("Europe/Zagreb", "Europe/Zagreb"), + ("Europe/Zaporozhye", "Europe/Zaporozhye"), + ("Europe/Zurich", "Europe/Zurich"), + ("GB", "GB"), + ("GB-Eire", "GB-Eire"), + ("GMT", "GMT"), + ("GMT+0", "GMT+0"), + ("GMT-0", "GMT-0"), + ("GMT0", "GMT0"), + ("Greenwich", "Greenwich"), + ("HST", "HST"), + ("Hongkong", "Hongkong"), + ("Iceland", "Iceland"), + ("Indian/Antananarivo", "Indian/Antananarivo"), + ("Indian/Chagos", "Indian/Chagos"), + ("Indian/Christmas", "Indian/Christmas"), + ("Indian/Cocos", "Indian/Cocos"), + ("Indian/Comoro", "Indian/Comoro"), + ("Indian/Kerguelen", "Indian/Kerguelen"), + ("Indian/Mahe", "Indian/Mahe"), + ("Indian/Maldives", "Indian/Maldives"), + ("Indian/Mauritius", "Indian/Mauritius"), + ("Indian/Mayotte", "Indian/Mayotte"), + ("Indian/Reunion", "Indian/Reunion"), + ("Iran", "Iran"), + ("Israel", "Israel"), + ("Jamaica", "Jamaica"), + ("Japan", "Japan"), + ("Kwajalein", "Kwajalein"), + ("Libya", "Libya"), + ("MET", "MET"), + ("MST", "MST"), + ("MST7MDT", "MST7MDT"), + ("Mexico/BajaNorte", "Mexico/BajaNorte"), + ("Mexico/BajaSur", "Mexico/BajaSur"), + ("Mexico/General", "Mexico/General"), + ("NZ", "NZ"), + ("NZ-CHAT", "NZ-CHAT"), + ("Navajo", "Navajo"), + ("PRC", "PRC"), + ("PST8PDT", "PST8PDT"), + ("Pacific/Apia", "Pacific/Apia"), + ("Pacific/Auckland", "Pacific/Auckland"), + ("Pacific/Bougainville", "Pacific/Bougainville"), + ("Pacific/Chatham", "Pacific/Chatham"), + ("Pacific/Chuuk", "Pacific/Chuuk"), + ("Pacific/Easter", "Pacific/Easter"), + ("Pacific/Efate", "Pacific/Efate"), + ("Pacific/Enderbury", "Pacific/Enderbury"), + ("Pacific/Fakaofo", "Pacific/Fakaofo"), + ("Pacific/Fiji", "Pacific/Fiji"), + ("Pacific/Funafuti", "Pacific/Funafuti"), + ("Pacific/Galapagos", "Pacific/Galapagos"), + ("Pacific/Gambier", "Pacific/Gambier"), + ("Pacific/Guadalcanal", "Pacific/Guadalcanal"), + ("Pacific/Guam", "Pacific/Guam"), + ("Pacific/Honolulu", "Pacific/Honolulu"), + ("Pacific/Johnston", "Pacific/Johnston"), + ("Pacific/Kanton", "Pacific/Kanton"), + ("Pacific/Kiritimati", "Pacific/Kiritimati"), + ("Pacific/Kosrae", "Pacific/Kosrae"), + ("Pacific/Kwajalein", "Pacific/Kwajalein"), + ("Pacific/Majuro", "Pacific/Majuro"), + ("Pacific/Marquesas", "Pacific/Marquesas"), + ("Pacific/Midway", "Pacific/Midway"), + ("Pacific/Nauru", "Pacific/Nauru"), + ("Pacific/Niue", "Pacific/Niue"), + ("Pacific/Norfolk", "Pacific/Norfolk"), + ("Pacific/Noumea", "Pacific/Noumea"), + ("Pacific/Pago_Pago", "Pacific/Pago_Pago"), + ("Pacific/Palau", "Pacific/Palau"), + ("Pacific/Pitcairn", "Pacific/Pitcairn"), + ("Pacific/Pohnpei", "Pacific/Pohnpei"), + ("Pacific/Ponape", "Pacific/Ponape"), + ("Pacific/Port_Moresby", "Pacific/Port_Moresby"), + ("Pacific/Rarotonga", "Pacific/Rarotonga"), + ("Pacific/Saipan", "Pacific/Saipan"), + ("Pacific/Samoa", "Pacific/Samoa"), + ("Pacific/Tahiti", "Pacific/Tahiti"), + ("Pacific/Tarawa", "Pacific/Tarawa"), + ("Pacific/Tongatapu", "Pacific/Tongatapu"), + ("Pacific/Truk", "Pacific/Truk"), + ("Pacific/Wake", "Pacific/Wake"), + ("Pacific/Wallis", "Pacific/Wallis"), + ("Pacific/Yap", "Pacific/Yap"), + ("Poland", "Poland"), + ("Portugal", "Portugal"), + ("ROC", "ROC"), + ("ROK", "ROK"), + ("Singapore", "Singapore"), + ("Turkey", "Turkey"), + ("UCT", "UCT"), + ("US/Alaska", "US/Alaska"), + ("US/Aleutian", "US/Aleutian"), + ("US/Arizona", "US/Arizona"), + ("US/Central", "US/Central"), + ("US/East-Indiana", "US/East-Indiana"), + ("US/Eastern", "US/Eastern"), + ("US/Hawaii", "US/Hawaii"), + ("US/Indiana-Starke", "US/Indiana-Starke"), + ("US/Michigan", "US/Michigan"), + ("US/Mountain", "US/Mountain"), + ("US/Pacific", "US/Pacific"), + ("US/Samoa", "US/Samoa"), + ("UTC", "UTC"), + ("Universal", "Universal"), + ("W-SU", "W-SU"), + ("WET", "WET"), + ("Zulu", "Zulu"), + ], + default="UTC", + max_length=255, + ), + ), + migrations.AlterField( + model_name="cycle", + name="end_date", + field=models.DateTimeField( + blank=True, null=True, verbose_name="End Date" + ), + ), + migrations.AlterField( + model_name="cycle", + name="start_date", + field=models.DateTimeField( + blank=True, null=True, verbose_name="Start Date" + ), + ), + migrations.CreateModel( + name="DraftIssueModule", + 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" + ), + ), + ( + "deleted_at", + models.DateTimeField( + blank=True, null=True, verbose_name="Deleted 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="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "draft_issue", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="draft_issue_module", + to="db.draftissue", + ), + ), + ( + "module", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="draft_issue_module", + to="db.module", + ), + ), + ( + "project", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ( + "workspace", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + ], + options={ + "verbose_name": "Draft Issue Module", + "verbose_name_plural": "Draft Issue Modules", + "db_table": "draft_issue_modules", + "ordering": ("-created_at",), + }, + ), + migrations.CreateModel( + name="DraftIssueLabel", + 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" + ), + ), + ( + "deleted_at", + models.DateTimeField( + blank=True, null=True, verbose_name="Deleted 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="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "draft_issue", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="draft_label_issue", + to="db.draftissue", + ), + ), + ( + "label", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="draft_label_issue", + to="db.label", + ), + ), + ( + "project", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ( + "workspace", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + ], + options={ + "verbose_name": "Draft Issue Label", + "verbose_name_plural": "Draft Issue Labels", + "db_table": "draft_issue_labels", + "ordering": ("-created_at",), + }, + ), + migrations.CreateModel( + name="DraftIssueCycle", + 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" + ), + ), + ( + "deleted_at", + models.DateTimeField( + blank=True, null=True, verbose_name="Deleted 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="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "cycle", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="draft_issue_cycle", + to="db.cycle", + ), + ), + ( + "draft_issue", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="draft_issue_cycle", + to="db.draftissue", + ), + ), + ( + "project", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ( + "workspace", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + ], + options={ + "verbose_name": "Draft Issue Cycle", + "verbose_name_plural": "Draft Issue Cycles", + "db_table": "draft_issue_cycles", + "ordering": ("-created_at",), + }, + ), + migrations.CreateModel( + name="DraftIssueAssignee", + 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" + ), + ), + ( + "deleted_at", + models.DateTimeField( + blank=True, null=True, verbose_name="Deleted At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "assignee", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="draft_issue_assignee", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "draft_issue", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="draft_issue_assignee", + to="db.draftissue", + ), + ), + ( + "project", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ( + "workspace", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + ], + options={ + "verbose_name": "Draft Issue Assignee", + "verbose_name_plural": "Draft Issue Assignees", + "db_table": "draft_issue_assignees", + "ordering": ("-created_at",), + }, + ), + migrations.AddField( + model_name="draftissue", + name="assignees", + field=models.ManyToManyField( + blank=True, + related_name="draft_assignee", + through="db.DraftIssueAssignee", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="draftissue", + name="created_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + migrations.AddField( + model_name="draftissue", + name="estimate_point", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="draft_issue_estimates", + to="db.estimatepoint", + ), + ), + migrations.AddField( + model_name="draftissue", + name="labels", + field=models.ManyToManyField( + blank=True, + related_name="draft_labels", + through="db.DraftIssueLabel", + to="db.label", + ), + ), + migrations.AddField( + model_name="draftissue", + name="parent", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="draft_parent_issue", + to="db.issue", + ), + ), + migrations.AddField( + model_name="draftissue", + name="project", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AddField( + model_name="draftissue", + name="state", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="state_draft_issue", + to="db.state", + ), + ), + migrations.AddField( + model_name="draftissue", + name="type", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="draft_issue_type", + to="db.issuetype", + ), + ), + migrations.AddField( + model_name="draftissue", + name="updated_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + migrations.AddField( + model_name="draftissue", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AddConstraint( + model_name="draftissuemodule", + constraint=models.UniqueConstraint( + condition=models.Q(("deleted_at__isnull", True)), + fields=("draft_issue", "module"), + name="module_draft_issue_unique_issue_module_when_deleted_at_null", + ), + ), + migrations.AlterUniqueTogether( + name="draftissuemodule", + unique_together={("draft_issue", "module", "deleted_at")}, + ), + migrations.AddConstraint( + model_name="draftissueassignee", + constraint=models.UniqueConstraint( + condition=models.Q(("deleted_at__isnull", True)), + fields=("draft_issue", "assignee"), + name="draft_issue_assignee_unique_issue_assignee_when_deleted_at_null", + ), + ), + migrations.AlterUniqueTogether( + name="draftissueassignee", + unique_together={("draft_issue", "assignee", "deleted_at")}, + ), + migrations.AddField( + model_name="cycle", + name="version", + field=models.IntegerField(default=1), + ), + migrations.RunPython(migrate_draft_issues), + ] diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index e7def641d5c..a6fa6dddb76 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -5,6 +5,7 @@ from .cycle import Cycle, CycleFavorite, CycleIssue, CycleUserProperties from .dashboard import Dashboard, DashboardWidget, Widget from .deploy_board import DeployBoard +from .draft import DraftIssue, DraftIssueAssignee, DraftIssueLabel, DraftIssueModule, DraftIssueCycle from .estimate import Estimate, EstimatePoint from .exporter import ExporterHistory from .importer import Importer diff --git a/apiserver/plane/db/models/cycle.py b/apiserver/plane/db/models/cycle.py index b3ce49e01a9..7c6ac8e3935 100644 --- a/apiserver/plane/db/models/cycle.py +++ b/apiserver/plane/db/models/cycle.py @@ -1,3 +1,6 @@ +# Python imports +import pytz + # Django imports from django.conf import settings from django.db import models @@ -55,10 +58,12 @@ class Cycle(ProjectBaseModel): description = models.TextField( verbose_name="Cycle Description", blank=True ) - start_date = models.DateField( + start_date = models.DateTimeField( verbose_name="Start Date", blank=True, null=True ) - end_date = models.DateField(verbose_name="End Date", blank=True, null=True) + end_date = models.DateTimeField( + verbose_name="End Date", blank=True, null=True + ) owned_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, @@ -71,6 +76,12 @@ class Cycle(ProjectBaseModel): progress_snapshot = models.JSONField(default=dict) archived_at = models.DateTimeField(null=True) logo_props = models.JSONField(default=dict) + # timezone + TIMEZONE_CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones)) + timezone = models.CharField( + max_length=255, default="UTC", choices=TIMEZONE_CHOICES + ) + version = models.IntegerField(default=1) class Meta: verbose_name = "Cycle" diff --git a/apiserver/plane/db/models/draft.py b/apiserver/plane/db/models/draft.py new file mode 100644 index 00000000000..671b89ff1fa --- /dev/null +++ b/apiserver/plane/db/models/draft.py @@ -0,0 +1,253 @@ +# Django imports +from django.conf import settings +from django.db import models +from django.utils import timezone + +# Module imports +from plane.utils.html_processor import strip_tags + +from .workspace import WorkspaceBaseModel + + +class DraftIssue(WorkspaceBaseModel): + PRIORITY_CHOICES = ( + ("urgent", "Urgent"), + ("high", "High"), + ("medium", "Medium"), + ("low", "Low"), + ("none", "None"), + ) + parent = models.ForeignKey( + "db.Issue", + on_delete=models.CASCADE, + null=True, + blank=True, + related_name="draft_parent_issue", + ) + state = models.ForeignKey( + "db.State", + on_delete=models.CASCADE, + null=True, + blank=True, + related_name="state_draft_issue", + ) + estimate_point = models.ForeignKey( + "db.EstimatePoint", + on_delete=models.SET_NULL, + related_name="draft_issue_estimates", + null=True, + blank=True, + ) + name = models.CharField( + max_length=255, verbose_name="Issue Name", blank=True, null=True + ) + description = models.JSONField(blank=True, default=dict) + description_html = models.TextField(blank=True, default="

") + description_stripped = models.TextField(blank=True, null=True) + description_binary = models.BinaryField(null=True) + priority = models.CharField( + max_length=30, + choices=PRIORITY_CHOICES, + verbose_name="Issue Priority", + default="none", + ) + start_date = models.DateField(null=True, blank=True) + target_date = models.DateField(null=True, blank=True) + assignees = models.ManyToManyField( + settings.AUTH_USER_MODEL, + blank=True, + related_name="draft_assignee", + through="DraftIssueAssignee", + through_fields=("draft_issue", "assignee"), + ) + labels = models.ManyToManyField( + "db.Label", + blank=True, + related_name="draft_labels", + through="DraftIssueLabel", + ) + sort_order = models.FloatField(default=65535) + completed_at = models.DateTimeField(null=True) + external_source = models.CharField(max_length=255, null=True, blank=True) + external_id = models.CharField(max_length=255, blank=True, null=True) + type = models.ForeignKey( + "db.IssueType", + on_delete=models.SET_NULL, + related_name="draft_issue_type", + null=True, + blank=True, + ) + + class Meta: + verbose_name = "DraftIssue" + verbose_name_plural = "DraftIssues" + db_table = "draft_issues" + ordering = ("-created_at",) + + def save(self, *args, **kwargs): + if self.state is None: + try: + from plane.db.models import State + + default_state = State.objects.filter( + ~models.Q(is_triage=True), + project=self.project, + default=True, + ).first() + if default_state is None: + random_state = State.objects.filter( + ~models.Q(is_triage=True), project=self.project + ).first() + self.state = random_state + else: + self.state = default_state + except ImportError: + pass + else: + try: + from plane.db.models import State + + if self.state.group == "completed": + self.completed_at = timezone.now() + else: + self.completed_at = None + except ImportError: + pass + + if self._state.adding: + # 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) + ) + largest_sort_order = DraftIssue.objects.filter( + project=self.project, state=self.state + ).aggregate(largest=models.Max("sort_order"))["largest"] + if largest_sort_order is not None: + self.sort_order = largest_sort_order + 10000 + + super(DraftIssue, self).save(*args, **kwargs) + + else: + # 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) + ) + super(DraftIssue, self).save(*args, **kwargs) + + def __str__(self): + """Return name of the draft issue""" + return f"{self.name} <{self.project.name}>" + + +class DraftIssueAssignee(WorkspaceBaseModel): + draft_issue = models.ForeignKey( + DraftIssue, + on_delete=models.CASCADE, + related_name="draft_issue_assignee", + ) + assignee = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="draft_issue_assignee", + ) + + class Meta: + unique_together = ["draft_issue", "assignee", "deleted_at"] + constraints = [ + models.UniqueConstraint( + fields=["draft_issue", "assignee"], + condition=models.Q(deleted_at__isnull=True), + name="draft_issue_assignee_unique_issue_assignee_when_deleted_at_null", + ) + ] + verbose_name = "Draft Issue Assignee" + verbose_name_plural = "Draft Issue Assignees" + db_table = "draft_issue_assignees" + ordering = ("-created_at",) + + def __str__(self): + return f"{self.draft_issue.name} {self.assignee.email}" + + +class DraftIssueLabel(WorkspaceBaseModel): + draft_issue = models.ForeignKey( + "db.DraftIssue", + on_delete=models.CASCADE, + related_name="draft_label_issue", + ) + label = models.ForeignKey( + "db.Label", on_delete=models.CASCADE, related_name="draft_label_issue" + ) + + class Meta: + verbose_name = "Draft Issue Label" + verbose_name_plural = "Draft Issue Labels" + db_table = "draft_issue_labels" + ordering = ("-created_at",) + + def __str__(self): + return f"{self.draft_issue.name} {self.label.name}" + + +class DraftIssueModule(WorkspaceBaseModel): + module = models.ForeignKey( + "db.Module", + on_delete=models.CASCADE, + related_name="draft_issue_module", + ) + draft_issue = models.ForeignKey( + "db.DraftIssue", + on_delete=models.CASCADE, + related_name="draft_issue_module", + ) + + class Meta: + unique_together = ["draft_issue", "module", "deleted_at"] + constraints = [ + models.UniqueConstraint( + fields=["draft_issue", "module"], + condition=models.Q(deleted_at__isnull=True), + name="module_draft_issue_unique_issue_module_when_deleted_at_null", + ) + ] + verbose_name = "Draft Issue Module" + verbose_name_plural = "Draft Issue Modules" + db_table = "draft_issue_modules" + ordering = ("-created_at",) + + def __str__(self): + return f"{self.module.name} {self.draft_issue.name}" + + +class DraftIssueCycle(WorkspaceBaseModel): + """ + Draft Issue Cycles + """ + + draft_issue = models.OneToOneField( + "db.DraftIssue", + on_delete=models.CASCADE, + related_name="draft_issue_cycle", + ) + cycle = models.ForeignKey( + "db.Cycle", on_delete=models.CASCADE, related_name="draft_issue_cycle" + ) + + class Meta: + verbose_name = "Draft Issue Cycle" + verbose_name_plural = "Draft Issue Cycles" + db_table = "draft_issue_cycles" + ordering = ("-created_at",) + + def __str__(self): + return f"{self.cycle}" diff --git a/apiserver/plane/db/models/project.py b/apiserver/plane/db/models/project.py index bcc16822739..3f784b399ec 100644 --- a/apiserver/plane/db/models/project.py +++ b/apiserver/plane/db/models/project.py @@ -1,4 +1,5 @@ # Python imports +import pytz from uuid import uuid4 # Django imports @@ -7,7 +8,7 @@ from django.db import models from django.db.models import Q -# Modeule imports +# Module imports from plane.db.mixins import AuditModel # Module imports @@ -119,6 +120,11 @@ class Project(BaseModel): related_name="default_state", ) archived_at = models.DateTimeField(null=True) + # timezone + TIMEZONE_CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones)) + timezone = models.CharField( + max_length=255, default="UTC", choices=TIMEZONE_CHOICES + ) def __str__(self): """Return name of the project""" diff --git a/apiserver/plane/utils/analytics_plot.py b/apiserver/plane/utils/analytics_plot.py index eda3b30ac9c..fd9f6405850 100644 --- a/apiserver/plane/utils/analytics_plot.py +++ b/apiserver/plane/utils/analytics_plot.py @@ -163,7 +163,7 @@ def burndown_plot( if queryset.end_date and queryset.start_date: # Get all dates between the two dates date_range = [ - queryset.start_date + timedelta(days=x) + (queryset.start_date + timedelta(days=x)).date() for x in range( (queryset.end_date - queryset.start_date).days + 1 ) @@ -203,7 +203,7 @@ def burndown_plot( if module_id: # Get all dates between the two dates date_range = [ - queryset.start_date + timedelta(days=x) + (queryset.start_date + timedelta(days=x)).date() for x in range( (queryset.target_date - queryset.start_date).days + 1 ) From 20c9e232e78aea10f5fa07f16e92af8c85a39538 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:53:07 +0530 Subject: [PATCH 08/18] chore: IssueParentDetail added to issue peekoverview (#5751) --- .../components/issues/peek-overview/issue-detail.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/web/core/components/issues/peek-overview/issue-detail.tsx b/web/core/components/issues/peek-overview/issue-detail.tsx index 20117d10f51..242ebfd0cee 100644 --- a/web/core/components/issues/peek-overview/issue-detail.tsx +++ b/web/core/components/issues/peek-overview/issue-detail.tsx @@ -1,7 +1,7 @@ import { FC, useEffect } from "react"; import { observer } from "mobx-react"; // components -import { TIssueOperations } from "@/components/issues"; +import { IssueParentDetail, TIssueOperations } from "@/components/issues"; // store hooks import { useIssueDetail, useUser } from "@/hooks/store"; // hooks @@ -57,6 +57,15 @@ export const PeekOverviewIssueDetails: FC = observer( return (
+ {issue.parent_id && ( + + )} Date: Tue, 8 Oct 2024 16:54:02 +0530 Subject: [PATCH 09/18] [PE-45] feat: page export as PDF & Markdown (#5705) * feat: export page as pdf and markdown * chore: add image conversion logic --- packages/editor/src/styles/editor.css | 2 +- packages/ui/src/modals/constants.ts | 3 + web/core/components/editor/index.ts | 1 + web/core/components/editor/pdf/document.tsx | 53 ++ web/core/components/editor/pdf/index.ts | 1 + .../pages/editor/header/options-dropdown.tsx | 59 +- web/core/components/pages/editor/title.tsx | 12 +- .../pages/modals/export-page-modal.tsx | 279 +++++++ web/core/components/pages/modals/index.ts | 1 + web/core/constants/editor.ts | 180 +++++ web/helpers/common.helper.ts | 2 + web/helpers/editor.helper.ts | 156 ++++ web/helpers/file.helper.ts | 31 + web/package.json | 2 + web/public/fonts/inter/bold-italic.ttf | Bin 0 -> 348180 bytes web/public/fonts/inter/bold.ttf | Bin 0 -> 344152 bytes web/public/fonts/inter/heavy-italic.ttf | Bin 0 -> 348712 bytes web/public/fonts/inter/heavy.ttf | Bin 0 -> 344820 bytes web/public/fonts/inter/light-italic.ttf | Bin 0 -> 347316 bytes web/public/fonts/inter/light.ttf | Bin 0 -> 343704 bytes web/public/fonts/inter/medium-italic.ttf | Bin 0 -> 346884 bytes web/public/fonts/inter/medium.ttf | Bin 0 -> 343200 bytes web/public/fonts/inter/regular-italic.ttf | Bin 0 -> 346480 bytes web/public/fonts/inter/regular.ttf | Bin 0 -> 342680 bytes web/public/fonts/inter/semibold-italic.ttf | Bin 0 -> 347760 bytes web/public/fonts/inter/semibold.ttf | Bin 0 -> 343828 bytes web/public/fonts/inter/thin-italic.ttf | Bin 0 -> 346916 bytes web/public/fonts/inter/thin.ttf | Bin 0 -> 343088 bytes web/public/fonts/inter/ultrabold-italic.ttf | Bin 0 -> 349064 bytes web/public/fonts/inter/ultrabold.ttf | Bin 0 -> 345008 bytes web/public/fonts/inter/ultralight-italic.ttf | Bin 0 -> 347452 bytes web/public/fonts/inter/ultralight.ttf | Bin 0 -> 343532 bytes yarn.lock | 692 +++++++++++++----- 33 files changed, 1278 insertions(+), 196 deletions(-) create mode 100644 web/core/components/editor/pdf/document.tsx create mode 100644 web/core/components/editor/pdf/index.ts create mode 100644 web/core/components/pages/modals/export-page-modal.tsx create mode 100644 web/helpers/editor.helper.ts create mode 100644 web/helpers/file.helper.ts create mode 100644 web/public/fonts/inter/bold-italic.ttf create mode 100644 web/public/fonts/inter/bold.ttf create mode 100644 web/public/fonts/inter/heavy-italic.ttf create mode 100644 web/public/fonts/inter/heavy.ttf create mode 100644 web/public/fonts/inter/light-italic.ttf create mode 100644 web/public/fonts/inter/light.ttf create mode 100644 web/public/fonts/inter/medium-italic.ttf create mode 100644 web/public/fonts/inter/medium.ttf create mode 100644 web/public/fonts/inter/regular-italic.ttf create mode 100644 web/public/fonts/inter/regular.ttf create mode 100644 web/public/fonts/inter/semibold-italic.ttf create mode 100644 web/public/fonts/inter/semibold.ttf create mode 100644 web/public/fonts/inter/thin-italic.ttf create mode 100644 web/public/fonts/inter/thin.ttf create mode 100644 web/public/fonts/inter/ultrabold-italic.ttf create mode 100644 web/public/fonts/inter/ultrabold.ttf create mode 100644 web/public/fonts/inter/ultralight-italic.ttf create mode 100644 web/public/fonts/inter/ultralight.ttf diff --git a/packages/editor/src/styles/editor.css b/packages/editor/src/styles/editor.css index e5047fb0c48..caffc0534a5 100644 --- a/packages/editor/src/styles/editor.css +++ b/packages/editor/src/styles/editor.css @@ -44,7 +44,7 @@ } &.sans-serif { - --font-style: sans-serif; + --font-style: "Inter", sans-serif; } &.serif { diff --git a/packages/ui/src/modals/constants.ts b/packages/ui/src/modals/constants.ts index 0cb268fc882..fe72ef7aea1 100644 --- a/packages/ui/src/modals/constants.ts +++ b/packages/ui/src/modals/constants.ts @@ -4,6 +4,9 @@ export enum EModalPosition { } export enum EModalWidth { + SM = "sm:max-w-sm", + MD = "sm:max-w-md", + LG = "sm:max-w-lg", XL = "sm:max-w-xl", XXL = "sm:max-w-2xl", XXXL = "sm:max-w-3xl", diff --git a/web/core/components/editor/index.ts b/web/core/components/editor/index.ts index 72e92a6a8ac..0b14bd13570 100644 --- a/web/core/components/editor/index.ts +++ b/web/core/components/editor/index.ts @@ -1,2 +1,3 @@ export * from "./lite-text-editor"; +export * from "./pdf"; export * from "./rich-text-editor"; diff --git a/web/core/components/editor/pdf/document.tsx b/web/core/components/editor/pdf/document.tsx new file mode 100644 index 00000000000..4dca9e6d53c --- /dev/null +++ b/web/core/components/editor/pdf/document.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { Document, Font, Page, PageProps } from "@react-pdf/renderer"; +import { Html } from "react-pdf-html"; +// constants +import { EDITOR_PDF_DOCUMENT_STYLESHEET } from "@/constants/editor"; + +Font.register({ + family: "Inter", + fonts: [ + { src: "/fonts/inter/thin.ttf", fontWeight: "thin" }, + { src: "/fonts/inter/thin.ttf", fontWeight: "thin", fontStyle: "italic" }, + { src: "/fonts/inter/ultralight.ttf", fontWeight: "ultralight" }, + { src: "/fonts/inter/ultralight.ttf", fontWeight: "ultralight", fontStyle: "italic" }, + { src: "/fonts/inter/light.ttf", fontWeight: "light" }, + { src: "/fonts/inter/light.ttf", fontWeight: "light", fontStyle: "italic" }, + { src: "/fonts/inter/regular.ttf", fontWeight: "normal" }, + { src: "/fonts/inter/regular.ttf", fontWeight: "normal", fontStyle: "italic" }, + { src: "/fonts/inter/medium.ttf", fontWeight: "medium" }, + { src: "/fonts/inter/medium.ttf", fontWeight: "medium", fontStyle: "italic" }, + { src: "/fonts/inter/semibold.ttf", fontWeight: "semibold" }, + { src: "/fonts/inter/semibold.ttf", fontWeight: "semibold", fontStyle: "italic" }, + { src: "/fonts/inter/bold.ttf", fontWeight: "bold" }, + { src: "/fonts/inter/bold.ttf", fontWeight: "bold", fontStyle: "italic" }, + { src: "/fonts/inter/extrabold.ttf", fontWeight: "ultrabold" }, + { src: "/fonts/inter/extrabold.ttf", fontWeight: "ultrabold", fontStyle: "italic" }, + { src: "/fonts/inter/heavy.ttf", fontWeight: "heavy" }, + { src: "/fonts/inter/heavy.ttf", fontWeight: "heavy", fontStyle: "italic" }, + ], +}); + +type Props = { + content: string; + pageFormat: PageProps["size"]; +}; + +export const PDFDocument: React.FC = (props) => { + const { content, pageFormat } = props; + + return ( + + + {content} + + + ); +}; diff --git a/web/core/components/editor/pdf/index.ts b/web/core/components/editor/pdf/index.ts new file mode 100644 index 00000000000..fe6d89c0eb9 --- /dev/null +++ b/web/core/components/editor/pdf/index.ts @@ -0,0 +1 @@ +export * from "./document"; diff --git a/web/core/components/pages/editor/header/options-dropdown.tsx b/web/core/components/pages/editor/header/options-dropdown.tsx index 0560002d840..c7cf53a5f50 100644 --- a/web/core/components/pages/editor/header/options-dropdown.tsx +++ b/web/core/components/pages/editor/header/options-dropdown.tsx @@ -1,12 +1,15 @@ "use client"; +import { useState } from "react"; import { observer } from "mobx-react"; import { useParams, useRouter } from "next/navigation"; -import { ArchiveRestoreIcon, Clipboard, Copy, History, Link, Lock, LockOpen } from "lucide-react"; +import { ArchiveRestoreIcon, ArrowUpToLine, Clipboard, Copy, History, Link, Lock, LockOpen } from "lucide-react"; // document editor import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor"; // ui import { ArchiveIcon, CustomMenu, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui"; +// components +import { ExportPageModal } from "@/components/pages"; // helpers import { copyTextToClipboard, copyUrlToClipboard } from "@/helpers/string.helper"; // hooks @@ -27,6 +30,7 @@ export const PageOptionsDropdown: React.FC = observer((props) => { const router = useRouter(); // store values const { + name, archived_at, is_locked, id, @@ -38,6 +42,8 @@ export const PageOptionsDropdown: React.FC = observer((props) => { canCurrentUserLockPage, restore, } = page; + // states + const [isExportModalOpen, setIsExportModalOpen] = useState(false); // store hooks const { workspaceSlug, projectId } = useParams(); // page filters @@ -157,26 +163,41 @@ export const PageOptionsDropdown: React.FC = observer((props) => { icon: History, shouldRender: true, }, + { + key: "export", + action: () => setIsExportModalOpen(true), + label: "Export", + icon: ArrowUpToLine, + shouldRender: true, + }, ]; return ( - - handleFullWidth(!isFullWidth)} - > - Full width - {}} /> - - {MENU_ITEMS.map((item) => { - if (!item.shouldRender) return null; - return ( - - - {item.label} - - ); - })} - + <> + setIsExportModalOpen(false)} + pageTitle={name ?? ""} + /> + + handleFullWidth(!isFullWidth)} + > + Full width + {}} /> + + {MENU_ITEMS.map((item) => { + if (!item.shouldRender) return null; + return ( + + + {item.label} + + ); + })} + + ); }); diff --git a/web/core/components/pages/editor/title.tsx b/web/core/components/pages/editor/title.tsx index 6792ebf0ac7..dafda1db3a9 100644 --- a/web/core/components/pages/editor/title.tsx +++ b/web/core/components/pages/editor/title.tsx @@ -1,6 +1,6 @@ "use client"; -import { CSSProperties, useState } from "react"; +import { useState } from "react"; import { observer } from "mobx-react"; // editor import { EditorRefApi } from "@plane/editor"; @@ -23,27 +23,21 @@ export const PageEditorTitle: React.FC = observer((props) => { // states const [isLengthVisible, setIsLengthVisible] = useState(false); // page filters - const { fontSize, fontStyle } = usePageFilters(); + const { fontSize } = usePageFilters(); // ui const titleClassName = cn("bg-transparent tracking-[-2%] font-semibold", { "text-[1.6rem] leading-[1.8rem]": fontSize === "small-font", "text-[2rem] leading-[2.25rem]": fontSize === "large-font", }); - const titleStyle: CSSProperties = { - fontFamily: fontStyle, - }; return ( <> {readOnly ? ( -
- {title} -
+
{title}
) : ( <>