From 4cc1b79d81f23b51d6200758e501d0595ed4902b Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Fri, 4 Oct 2024 21:35:13 +0530 Subject: [PATCH 01/97] chore: instance tracing --- apiserver/plane/license/bgtasks/tracer.py | 96 +++++++++++++++++++ .../management/commands/register_instance.py | 73 +++++--------- apiserver/plane/settings/common.py | 1 + apiserver/plane/utils/tracer.py | 26 ----- 4 files changed, 121 insertions(+), 75 deletions(-) create mode 100644 apiserver/plane/license/bgtasks/tracer.py delete mode 100644 apiserver/plane/utils/tracer.py diff --git a/apiserver/plane/license/bgtasks/tracer.py b/apiserver/plane/license/bgtasks/tracer.py new file mode 100644 index 0000000000..f350e9a45b --- /dev/null +++ b/apiserver/plane/license/bgtasks/tracer.py @@ -0,0 +1,96 @@ +# Third party imports +from celery import shared_task +from opentelemetry import trace + +# Module imports +from plane.license.models import Instance +from plane.db.models import ( + User, + Workspace, + Project, + Issue, + Module, + Cycle, + CycleIssue, + ModuleIssue, + Page, + WorkspaceMember, +) + + +@shared_task +def instance_traces(): + # Get the tracer + tracer = trace.get_tracer(__name__) + + # Check if the instance is registered + instance = Instance.objects.first() + + # If instance is None then return + if instance is None: + return + + if instance.is_telemetry_enabled: + # Instance details + with tracer.start_as_current_span("instance_details") as span: + # Count of all models + workspace_count = Workspace.objects.count() + user_count = User.objects.count() + project_count = Project.objects.count() + issue_count = Issue.objects.count() + module_count = Module.objects.count() + cycle_count = Cycle.objects.count() + cycle_issue_count = CycleIssue.objects.count() + module_issue_count = ModuleIssue.objects.count() + page_count = Page.objects.count() + + # Set span attributes + span.set_attribute("instance_id", instance.instance_id) + span.set_attribute("instance_name", instance.instance_name) + span.set_attribute("current_version", instance.current_version) + span.set_attribute("latest_version", instance.latest_version) + span.set_attribute( + "is_telemetry_enabled", instance.is_telemetry_enabled + ) + span.set_attribute("user_count", user_count) + span.set_attribute("workspace_count", workspace_count) + span.set_attribute("project_count", project_count) + span.set_attribute("issue_count", issue_count) + span.set_attribute("module_count", module_count) + span.set_attribute("cycle_count", cycle_count) + span.set_attribute("cycle_issue_count", cycle_issue_count) + span.set_attribute("module_issue_count", module_issue_count) + span.set_attribute("page_count", page_count) + + # Workspace details + for workspace in Workspace.objects.all(): + # Count of all models + project_count = Project.objects.filter(workspace=workspace).count() + issue_count = Issue.objects.filter(workspace=workspace).count() + module_count = Module.objects.filter(workspace=workspace).count() + cycle_count = Cycle.objects.filter(workspace=workspace).count() + cycle_issue_count = CycleIssue.objects.filter( + workspace=workspace + ).count() + module_issue_count = ModuleIssue.objects.filter( + workspace=workspace + ).count() + page_count = Page.objects.filter(workspace=workspace).count() + member_count = WorkspaceMember.objects.filter( + workspace=workspace + ).count() + + # Set span attributes + with tracer.start_as_current_span("workspace_details") as span: + span.set_attribute("workspace_id", str(workspace.id)) + span.set_attribute("workspace_slug", workspace.slug) + span.set_attribute("project_count", project_count) + span.set_attribute("issue_count", issue_count) + span.set_attribute("module_count", module_count) + span.set_attribute("cycle_count", cycle_count) + span.set_attribute("cycle_issue_count", cycle_issue_count) + span.set_attribute("module_issue_count", module_issue_count) + span.set_attribute("page_count", page_count) + span.set_attribute("member_count", member_count) + + return diff --git a/apiserver/plane/license/management/commands/register_instance.py b/apiserver/plane/license/management/commands/register_instance.py index 314d475200..09d13441c6 100644 --- a/apiserver/plane/license/management/commands/register_instance.py +++ b/apiserver/plane/license/management/commands/register_instance.py @@ -11,19 +11,8 @@ from django.conf import settings from plane.license.models import Instance from plane.db.models import ( User, - Workspace, - Project, - Issue, - Module, - Cycle, - CycleIssue, - ModuleIssue, - Page, ) - -from opentelemetry import trace - -tracer = trace.get_tracer(__name__) +from plane.license.bgtasks.tracer import instance_traces class Command(BaseCommand): @@ -35,16 +24,24 @@ class Command(BaseCommand): "machine_signature", type=str, help="Machine signature" ) + def read_package_json(self): + with open("package.json", "r") as file: + # Load JSON content from the file + data = json.load(file) + + payload = { + "instance_key": settings.INSTANCE_KEY, + "version": data.get("version", 0.1), + "user_count": User.objects.filter(is_bot=False).count(), + } + return payload + def handle(self, *args, **options): # Check if the instance is registered instance = Instance.objects.first() # If instance is None then register this instance if instance is None: - with open("package.json", "r") as file: - # Load JSON content from the file - data = json.load(file) - machine_signature = options.get( "machine_signature", "machine-signature" ) @@ -52,12 +49,7 @@ class Command(BaseCommand): if not machine_signature: raise CommandError("Machine signature is required") - payload = { - "instance_key": settings.INSTANCE_KEY, - "version": data.get("version", 0.1), - "machine_signature": machine_signature, - "user_count": User.objects.filter(is_bot=False).count(), - } + payload = self.read_package_json() instance = Instance.objects.create( instance_name="Plane Community Edition", @@ -74,32 +66,15 @@ class Command(BaseCommand): self.stdout.write( self.style.SUCCESS("Instance already registered") ) + payload = self.read_package_json() + # Update the instance details + instance.last_checked_at = timezone.now() + instance.user_count = payload.get("user_count", 0) + instance.current_version = payload.get("version") + instance.latest_version = payload.get("version") + instance.save() - if instance.is_telemetry_enabled: - with tracer.start_as_current_span("instance_details") as span: - workspace_count = Workspace.objects.count() - user_count = User.objects.count() - project_count = Project.objects.count() - issue_count = Issue.objects.count() - module_count = Module.objects.count() - cycle_count = Cycle.objects.count() - cycle_issue_count = CycleIssue.objects.count() - module_issue_count = ModuleIssue.objects.count() - page_count = Page.objects.count() + # Call the instance traces task + instance_traces.delay() - span.set_attribute("instance_id", instance.instance_id) - span.set_attribute("instance_name", instance.instance_name) - span.set_attribute("current_version", instance.current_version) - span.set_attribute("latest_version", instance.latest_version) - span.set_attribute( - "is_telemetry_enabled", instance.is_telemetry_enabled - ) - span.set_attribute("user_count", user_count) - span.set_attribute("workspace_count", workspace_count) - span.set_attribute("project_count", project_count) - span.set_attribute("issue_count", issue_count) - span.set_attribute("module_count", module_count) - span.set_attribute("cycle_count", cycle_count) - span.set_attribute("cycle_issue_count", cycle_issue_count) - span.set_attribute("module_issue_count", module_issue_count) - span.set_attribute("page_count", page_count) + return diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 2504cc3e36..5e1ea39a43 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -303,6 +303,7 @@ CELERY_IMPORTS = ( "plane.bgtasks.file_asset_task", "plane.bgtasks.email_notification_task", "plane.bgtasks.api_logs_task", + "plane.license.bgtasks.tracer", # management tasks "plane.bgtasks.dummy_data_task", ) diff --git a/apiserver/plane/utils/tracer.py b/apiserver/plane/utils/tracer.py deleted file mode 100644 index 831472cbfd..0000000000 --- a/apiserver/plane/utils/tracer.py +++ /dev/null @@ -1,26 +0,0 @@ -from opentelemetry import trace -from django.conf import settings -from functools import wraps - -tracer = trace.get_tracer(__name__) - - -def trace_operation(operation_name, **attributes): - def wrapper(func): - @wraps(func) - def traced_func(*args, **kwargs): - if settings.TELEMETRY_ENABLED: - with tracer.start_as_current_span(operation_name) as span: - for key, value in attributes.items(): - span.set_attribute(key, value) - result = func(*args, **kwargs) - span.add_event( - "operation_completed", {"result": str(result)} - ) - return result - else: - return func(*args, **kwargs) - - return traced_func - - return wrapper From dfd3af13cfac99603233b4422a2b600cbe947b59 Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Mon, 7 Oct 2024 12:57:15 +0530 Subject: [PATCH 02/97] fix: handled favorite entity data null (#5756) --- .../components/workspace/sidebar/favorites/favorites-menu.tsx | 2 +- web/core/hooks/use-favorite-item-details.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx b/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx index a1706e9368..89cd6f36f3 100644 --- a/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx +++ b/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx @@ -182,7 +182,7 @@ export const SidebarFavoritesMenu = observer(() => { .map((fav, index) => ( { const favoriteItemId = favorite?.entity_data?.id; const favoriteItemLogoProps = favorite?.entity_data?.logo_props; - const favoriteItemName = favorite?.entity_data.name || favorite?.name; + const favoriteItemName = favorite?.entity_data?.name || favorite?.name; const favoriteItemEntityType = favorite?.entity_type; // store hooks From 5f7b6ecf7fa4da93eaa4c98471b0f69684aac617 Mon Sep 17 00:00:00 2001 From: "M. Palanikannan" <73993394+Palanikannan1437@users.noreply.github.com> Date: Mon, 7 Oct 2024 13:12:16 +0530 Subject: [PATCH 03/97] fix: image deletion on submit fixed in comments (#5748) * fix: image deletion on submit fixed in comments * fix: cleareditor added to read only editor * fix: image component double drop fixed * feat: multiple image selection and uploading * fix: click event on read only instance * fix: made things async * fix: prevented default behaviour * fix: removed extra dep and cleaned up logic --- .../custom-image/components/image-block.tsx | 35 ++++++-- .../components/image-uploader.tsx | 44 +++++----- packages/editor/src/core/extensions/drop.tsx | 13 +-- packages/editor/src/core/hooks/use-editor.ts | 2 +- .../editor/src/core/hooks/use-file-upload.ts | 80 ++++++++++++------- .../src/core/hooks/use-read-only-editor.ts | 4 +- .../src/core/plugins/image/delete-image.ts | 2 + 7 files changed, 105 insertions(+), 75 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 42b51de5fb..f067ee9478 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 @@ -71,6 +71,17 @@ export const CustomImageBlock: React.FC = (props) => { const containerRect = useRef(null); const imageRef = useRef(null); + const updateAttributesSafely = useCallback( + (attributes: Partial, errorMessage: string) => { + try { + updateAttributes(attributes); + } catch (error) { + console.error(`${errorMessage}:`, error); + } + }, + [updateAttributes] + ); + const handleImageLoad = useCallback(() => { const img = imageRef.current; if (!img) return; @@ -105,17 +116,25 @@ export const CustomImageBlock: React.FC = (props) => { }; setSize(initialComputedSize); - updateAttributes(initialComputedSize); + updateAttributesSafely( + initialComputedSize, + "Failed to update attributes while initializing an image for the first time:" + ); } else { // as the aspect ratio in not stored for old images, we need to update the attrs - setSize((prevSize) => { - const newSize = { ...prevSize, aspectRatio }; - updateAttributes(newSize); - return newSize; - }); + if (!aspectRatio) { + setSize((prevSize) => { + const newSize = { ...prevSize, aspectRatio }; + updateAttributesSafely( + newSize, + "Failed to update attributes while initializing images with width but no aspect ratio:" + ); + return newSize; + }); + } } setInitialResizeComplete(true); - }, [width, updateAttributes, editorContainer]); + }, [width, updateAttributes, editorContainer, aspectRatio]); // for real time resizing useLayoutEffect(() => { @@ -142,7 +161,7 @@ export const CustomImageBlock: React.FC = (props) => { const handleResizeEnd = useCallback(() => { setIsResizing(false); - updateAttributes(size); + updateAttributesSafely(size, "Failed to update attributes at the end of resizing:"); }, [size, updateAttributes]); const handleResizeStart = useCallback((e: React.MouseEvent | React.TouchEvent) => { diff --git a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx index 89cf36ca52..b5c52db66c 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx @@ -5,9 +5,7 @@ import { ImageIcon } from "lucide-react"; // helpers import { cn } from "@/helpers/common"; // hooks -import { useUploader, useDropZone } from "@/hooks/use-file-upload"; -// plugins -import { isFileValid } from "@/plugins/image"; +import { useUploader, useDropZone, uploadFirstImageAndInsertRemaining } from "@/hooks/use-file-upload"; // extensions import { getImageComponentImageFileMap, ImageAttributes } from "@/extensions/custom-image"; @@ -74,7 +72,11 @@ export const CustomImageUploader = (props: { ); // hooks const { uploading: isImageBeingUploaded, uploadFile } = useUploader({ onUpload, editor, loadImageFromFileSystem }); - const { draggedInside, onDrop, onDragEnter, onDragLeave } = useDropZone({ uploader: uploadFile }); + const { draggedInside, onDrop, onDragEnter, onDragLeave } = useDropZone({ + uploader: uploadFile, + editor, + pos: getPos(), + }); // the meta data of the image component const meta = useMemo( @@ -82,9 +84,6 @@ export const CustomImageUploader = (props: { [imageComponentImageFileMap, imageEntityId] ); - // if the image component is dropped, we check if it has an existing file - const existingFile = useMemo(() => (meta && meta.event === "drop" ? meta.file : undefined), [meta]); - // after the image component is mounted we start the upload process based on // it's uploaded useEffect(() => { @@ -100,27 +99,20 @@ export const CustomImageUploader = (props: { } }, [meta, uploadFile, imageComponentImageFileMap]); - // check if the image is dropped and set the local image as the existing file - useEffect(() => { - if (existingFile) { - uploadFile(existingFile); - } - }, [existingFile, uploadFile]); - const onFileChange = useCallback( - (e: ChangeEvent) => { - const file = e.target.files?.[0]; - if (file) { - if (isFileValid(file)) { - uploadFile(file); - } + async (e: ChangeEvent) => { + e.preventDefault(); + const fileList = e.target.files; + if (!fileList) { + return; } + await uploadFirstImageAndInsertRemaining(editor, fileList, getPos(), uploadFile); }, - [uploadFile] + [uploadFile, editor, getPos] ); const getDisplayMessage = useCallback(() => { - const isUploading = isImageBeingUploaded || existingFile; + const isUploading = isImageBeingUploaded; if (failedToLoadImage) { return "Error loading image"; } @@ -134,13 +126,14 @@ export const CustomImageUploader = (props: { } return "Add an image"; - }, [draggedInside, failedToLoadImage, existingFile, isImageBeingUploaded]); + }, [draggedInside, failedToLoadImage, isImageBeingUploaded]); return (
{ - if (!failedToLoadImage) { + if (!failedToLoadImage && editor.isEditable) { fileInputRef.current?.click(); } }} @@ -167,6 +160,7 @@ export const CustomImageUploader = (props: { type="file" accept=".jpg,.jpeg,.png,.webp" onChange={onFileChange} + multiple />
); diff --git a/packages/editor/src/core/extensions/drop.tsx b/packages/editor/src/core/extensions/drop.tsx index 8d66a5f9f5..2044f03bf5 100644 --- a/packages/editor/src/core/extensions/drop.tsx +++ b/packages/editor/src/core/extensions/drop.tsx @@ -21,7 +21,7 @@ export const DropHandlerExtension = () => if (imageFiles.length > 0) { const pos = view.state.selection.from; - insertImages({ editor, files: imageFiles, initialPos: pos, event: "drop" }); + insertImagesSafely({ editor, files: imageFiles, initialPos: pos, event: "drop" }); } return true; } @@ -41,7 +41,7 @@ export const DropHandlerExtension = () => if (coordinates) { const pos = coordinates.pos; - insertImages({ editor, files: imageFiles, initialPos: pos, event: "drop" }); + insertImagesSafely({ editor, files: imageFiles, initialPos: pos, event: "drop" }); } return true; } @@ -54,7 +54,7 @@ export const DropHandlerExtension = () => }, }); -const insertImages = async ({ +export const insertImagesSafely = async ({ editor, files, initialPos, @@ -72,13 +72,6 @@ const insertImages = async ({ const docSize = editor.state.doc.content.size; pos = Math.min(pos, docSize); - // Check if the position has a non-empty node - const nodeAtPos = editor.state.doc.nodeAt(pos); - if (nodeAtPos && nodeAtPos.content.size > 0) { - // Move to the end of the current node - pos += nodeAtPos.nodeSize; - } - try { // Insert the image at the current position editor.commands.insertImageComponent({ file, pos, event }); diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index fee3306123..65e36c01ae 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -126,7 +126,7 @@ export const useEditor = (props: CustomEditorProps) => { forwardedRef, () => ({ clearEditor: (emitUpdate = false) => { - editorRef.current?.commands.clearContent(emitUpdate); + editorRef.current?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run(); }, setEditorValue: (content: string) => { editorRef.current?.commands.setContent(content, false, { preserveWhitespace: "full" }); diff --git a/packages/editor/src/core/hooks/use-file-upload.ts b/packages/editor/src/core/hooks/use-file-upload.ts index 5dfa025e59..f1bc8c8a13 100644 --- a/packages/editor/src/core/hooks/use-file-upload.ts +++ b/packages/editor/src/core/hooks/use-file-upload.ts @@ -1,6 +1,7 @@ import { DragEvent, useCallback, useEffect, useState } from "react"; import { Editor } from "@tiptap/core"; import { isFileValid } from "@/plugins/image"; +import { insertImagesSafely } from "@/extensions/drop"; export const useUploader = ({ onUpload, @@ -63,7 +64,15 @@ export const useUploader = ({ return { uploading, uploadFile }; }; -export const useDropZone = ({ uploader }: { uploader: (file: File) => void }) => { +export const useDropZone = ({ + uploader, + editor, + pos, +}: { + uploader: (file: File) => Promise; + editor: Editor; + pos: number; +}) => { const [isDragging, setIsDragging] = useState(false); const [draggedInside, setDraggedInside] = useState(false); @@ -86,40 +95,16 @@ export const useDropZone = ({ uploader }: { uploader: (file: File) => void }) => }, []); const onDrop = useCallback( - (e: DragEvent) => { + async (e: DragEvent) => { + e.preventDefault(); setDraggedInside(false); if (e.dataTransfer.files.length === 0) { return; } - const fileList = e.dataTransfer.files; - - const files: File[] = []; - - for (let i = 0; i < fileList.length; i += 1) { - const item = fileList.item(i); - if (item) { - files.push(item); - } - } - - if (files.some((file) => file.type.indexOf("image") === -1)) { - return; - } - - e.preventDefault(); - - const filteredFiles = files.filter((f) => f.type.indexOf("image") !== -1); - - const file = filteredFiles.length > 0 ? filteredFiles[0] : undefined; - - if (file) { - uploader(file); - } else { - console.error("No file found"); - } + await uploadFirstImageAndInsertRemaining(editor, fileList, pos, uploader); }, - [uploader] + [uploader, editor, pos] ); const onDragEnter = () => { @@ -143,3 +128,40 @@ function trimFileName(fileName: string, maxLength = 100) { return fileName; } + +// Upload the first image and insert the remaining images for uploading multiple image +// post insertion of image-component +export async function uploadFirstImageAndInsertRemaining( + editor: Editor, + fileList: FileList, + pos: number, + uploaderFn: (file: File) => Promise +) { + const filteredFiles: File[] = []; + for (let i = 0; i < fileList.length; i += 1) { + const item = fileList.item(i); + if (item && item.type.indexOf("image") !== -1 && isFileValid(item)) { + filteredFiles.push(item); + } + } + if (filteredFiles.length !== fileList.length) { + console.warn("Some files were not images and have been ignored."); + } + if (filteredFiles.length === 0) { + console.error("No image files found to upload"); + return; + } + + // Upload the first image + const firstFile = filteredFiles[0]; + uploaderFn(firstFile); + + // Insert the remaining images + const remainingFiles = filteredFiles.slice(1); + + if (remainingFiles.length > 0) { + const docSize = editor.state.doc.content.size; + const posOfNextImageToBeInserted = Math.min(pos + 1, docSize); + insertImagesSafely({ editor, files: remainingFiles, initialPos: posOfNextImageToBeInserted, event: "drop" }); + } +} diff --git a/packages/editor/src/core/hooks/use-read-only-editor.ts b/packages/editor/src/core/hooks/use-read-only-editor.ts index add0508b99..6d1ed6fa9f 100644 --- a/packages/editor/src/core/hooks/use-read-only-editor.ts +++ b/packages/editor/src/core/hooks/use-read-only-editor.ts @@ -70,8 +70,8 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => { const editorRef: MutableRefObject = useRef(null); useImperativeHandle(forwardedRef, () => ({ - clearEditor: () => { - editorRef.current?.commands.clearContent(); + clearEditor: (emitUpdate = false) => { + editorRef.current?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run(); }, setEditorValue: (content: string) => { editorRef.current?.commands.setContent(content, false, { preserveWhitespace: "full" }); diff --git a/packages/editor/src/core/plugins/image/delete-image.ts b/packages/editor/src/core/plugins/image/delete-image.ts index 72bb913ae7..21c8cd24f6 100644 --- a/packages/editor/src/core/plugins/image/delete-image.ts +++ b/packages/editor/src/core/plugins/image/delete-image.ts @@ -17,6 +17,8 @@ export const TrackImageDeletionPlugin = (editor: Editor, deleteImage: DeleteImag }); transactions.forEach((transaction) => { + // if the transaction has meta of skipImageDeletion get to true, then return (like while clearing the editor content programatically) + if (transaction.getMeta("skipImageDeletion")) return; // transaction could be a selection if (!transaction.docChanged) return; From 7bb04003ea6249fd1d20c2fe91a9d69b204f4404 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 7 Oct 2024 15:56:27 +0530 Subject: [PATCH 04/97] fix: instance trace --- apiserver/plane/license/bgtasks/tracer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apiserver/plane/license/bgtasks/tracer.py b/apiserver/plane/license/bgtasks/tracer.py index f350e9a45b..26efb45ed2 100644 --- a/apiserver/plane/license/bgtasks/tracer.py +++ b/apiserver/plane/license/bgtasks/tracer.py @@ -82,6 +82,7 @@ def instance_traces(): # Set span attributes with tracer.start_as_current_span("workspace_details") as span: + span.set_attribute("instance_id", instance.instance_id) span.set_attribute("workspace_id", str(workspace.id)) span.set_attribute("workspace_slug", workspace.slug) span.set_attribute("project_count", project_count) From c92fe6191e7cbd6101f181d5c893195ea500f8bc Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Mon, 7 Oct 2024 17:23:37 +0530 Subject: [PATCH 05/97] [WEB-2600] fix: estimate point deletion (#5762) * chore: only delete the cascade fields * chore: logged the issue activity --- apiserver/plane/app/views/estimate/base.py | 64 ++++++++++++++++++- apiserver/plane/app/views/project/base.py | 2 +- apiserver/plane/bgtasks/deletion_task.py | 34 ++++++---- .../plane/bgtasks/issue_activities_task.py | 10 +-- 4 files changed, 87 insertions(+), 23 deletions(-) diff --git a/apiserver/plane/app/views/estimate/base.py b/apiserver/plane/app/views/estimate/base.py index f384880019..80943c05eb 100644 --- a/apiserver/plane/app/views/estimate/base.py +++ b/apiserver/plane/app/views/estimate/base.py @@ -1,5 +1,9 @@ import random import string +import json + +# Django imports +from django.utils import timezone # Third party imports from rest_framework.response import Response @@ -19,6 +23,7 @@ from plane.app.serializers import ( EstimateReadSerializer, ) from plane.utils.cache import invalidate_cache +from plane.bgtasks.issue_activities_task import issue_activity def generate_random_name(length=10): @@ -249,11 +254,66 @@ class EstimatePointEndpoint(BaseViewSet): ) # update all the issues with the new estimate if new_estimate_id: - _ = Issue.objects.filter( + issues = Issue.objects.filter( project_id=project_id, workspace__slug=slug, estimate_point_id=estimate_point_id, - ).update(estimate_point_id=new_estimate_id) + ) + for issue in issues: + issue_activity.delay( + type="issue.activity.updated", + requested_data=json.dumps( + { + "estimate_point": ( + str(new_estimate_id) + if new_estimate_id + else None + ), + } + ), + actor_id=str(request.user.id), + issue_id=issue.id, + project_id=str(project_id), + current_instance=json.dumps( + { + "estimate_point": ( + str(issue.estimate_point_id) + if issue.estimate_point_id + else None + ), + } + ), + epoch=int(timezone.now().timestamp()), + ) + issues.update(estimate_point_id=new_estimate_id) + else: + issues = Issue.objects.filter( + project_id=project_id, + workspace__slug=slug, + estimate_point_id=estimate_point_id, + ) + for issue in issues: + issue_activity.delay( + type="issue.activity.updated", + requested_data=json.dumps( + { + "estimate_point": None, + } + ), + actor_id=str(request.user.id), + issue_id=issue.id, + project_id=str(project_id), + current_instance=json.dumps( + { + "estimate_point": ( + str(issue.estimate_point_id) + if issue.estimate_point_id + else None + ), + } + ), + epoch=int(timezone.now().timestamp()), + ) # delete the estimate point old_estimate_point = EstimatePoint.objects.filter( diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py index 8bdff03e62..6a9afb6523 100644 --- a/apiserver/plane/app/views/project/base.py +++ b/apiserver/plane/app/views/project/base.py @@ -413,7 +413,7 @@ class ProjectViewSet(BaseViewSet): status=status.HTTP_410_GONE, ) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") + @allow_permission([ROLE.ADMIN]) def partial_update(self, request, slug, pk=None): try: workspace = Workspace.objects.get(slug=slug) diff --git a/apiserver/plane/bgtasks/deletion_task.py b/apiserver/plane/bgtasks/deletion_task.py index d8272591a9..35d72812da 100644 --- a/apiserver/plane/bgtasks/deletion_task.py +++ b/apiserver/plane/bgtasks/deletion_task.py @@ -2,6 +2,7 @@ from django.utils import timezone from django.apps import apps from django.conf import settings +from django.db import models from django.core.exceptions import ObjectDoesNotExist # Third party imports @@ -18,17 +19,25 @@ def soft_delete_related_objects( for field in related_fields: if field.one_to_many or field.one_to_one: try: - if field.one_to_many: - related_objects = getattr(instance, field.name).all() - elif field.one_to_one: - related_object = getattr(instance, field.name) - related_objects = ( - [related_object] if related_object is not None else [] - ) - for obj in related_objects: - if obj: - obj.deleted_at = timezone.now() - obj.save(using=using) + # Check if the field has CASCADE on delete + if ( + hasattr(field.remote_field, "on_delete") + and field.remote_field.on_delete == models.CASCADE + ): + if field.one_to_many: + related_objects = getattr(instance, field.name).all() + elif field.one_to_one: + related_object = getattr(instance, field.name) + related_objects = ( + [related_object] + if related_object is not None + else [] + ) + + for obj in related_objects: + if obj: + obj.deleted_at = timezone.now() + obj.save(using=using) except ObjectDoesNotExist: pass @@ -154,8 +163,7 @@ def hard_delete(): if hasattr(model, "deleted_at"): # Get all instances where 'deleted_at' is greater than 30 days ago _ = model.all_objects.filter( - deleted_at__lt=timezone.now() - - timezone.timedelta(days=days) + deleted_at__lt=timezone.now() - timezone.timedelta(days=days) ).delete() return diff --git a/apiserver/plane/bgtasks/issue_activities_task.py b/apiserver/plane/bgtasks/issue_activities_task.py index 8ecf7845db..0cee9baef3 100644 --- a/apiserver/plane/bgtasks/issue_activities_task.py +++ b/apiserver/plane/bgtasks/issue_activities_task.py @@ -465,7 +465,7 @@ def track_estimate_points( IssueActivity( issue_id=issue_id, actor_id=actor_id, - verb="updated", + verb="removed" if new_estimate is None else "updated", old_identifier=( current_instance.get("estimate_point") if current_instance.get("estimate_point") is not None @@ -1700,16 +1700,12 @@ def issue_activity( event=( "issue_comment" if activity.field == "comment" - else "inbox_issue" - if inbox - else "issue" + else "inbox_issue" if inbox else "issue" ), event_id=( activity.issue_comment_id if activity.field == "comment" - else inbox - if inbox - else activity.issue_id + else inbox if inbox else activity.issue_id ), verb=activity.verb, field=( 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 06/97] 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 6a9afb6523..f5ddb22458 100644 --- a/apiserver/plane/app/views/project/base.py +++ b/apiserver/plane/app/views/project/base.py @@ -413,9 +413,20 @@ class ProjectViewSet(BaseViewSet): 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 07/97] [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 1182feeb0a..e4b4dc6651 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 08/97] 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 616e315e20..5ab6fbdf5b 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 ba390180bb..809802b4f9 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 09/97] [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 27e33e2c2c..aa81ae5816 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 a1255b206c..025c9fab04 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 f67df642c7..0edfa2b5a2 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 2db3713667..6f69e8f50a 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 3d00e34eba..22dc8817a9 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 bfec73dae0..8d26ab5f4e 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 10/97] [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 ef19a3342c..775ff637b0 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 f12821c5a1..fa60c354e3 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 fd3bd07afa..9b0638c1f6 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 11/97] 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 f067ee9478..ed60f3dab0 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 12/97] [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 3814466322..be23256ac6 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -207,7 +207,7 @@ class CycleAPIEndpoint(BaseAPIView): # 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 @@ class CycleAPIEndpoint(BaseAPIView): 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 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView): 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 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView): 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 618a9ec20f..b3c3d79499 100644 --- a/apiserver/plane/app/serializers/__init__.py +++ b/apiserver/plane/app/serializers/__init__.py @@ -124,3 +124,9 @@ from .webhook import WebhookSerializer, WebhookLogSerializer 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 0000000000..1ca9a74312 --- /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 564725e839..e6007862fd 100644 --- a/apiserver/plane/app/urls/issue.py +++ b/apiserver/plane/app/urls/issue.py @@ -11,7 +11,6 @@ from plane.app.views import ( IssueActivityEndpoint, IssueArchiveViewSet, IssueCommentViewSet, - IssueDraftViewSet, IssueListEndpoint, IssueReactionViewSet, IssueRelationViewSet, @@ -290,28 +289,6 @@ urlpatterns = [ 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 3f1e000e47..7dab175441 100644 --- a/apiserver/plane/app/urls/workspace.py +++ b/apiserver/plane/app/urls/workspace.py @@ -27,6 +27,7 @@ from plane.app.views import ( WorkspaceCyclesEndpoint, WorkspaceFavoriteEndpoint, WorkspaceFavoriteGroupEndpoint, + WorkspaceDraftIssueViewSet, ) @@ -254,4 +255,25 @@ urlpatterns = [ 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 6c4cc12c89..872b511a04 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -40,6 +40,8 @@ from .workspace.base import ( ExportWorkspaceUserActivityEndpoint, ) +from .workspace.draft import WorkspaceDraftIssueViewSet + from .workspace.favorite import ( WorkspaceFavoriteEndpoint, WorkspaceFavoriteGroupEndpoint, @@ -133,8 +135,6 @@ from .issue.comment import ( 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 25ad8a2eb6..9d7f79b0ec 100644 --- a/apiserver/plane/app/views/cycle/archive.py +++ b/apiserver/plane/app/views/cycle/archive.py @@ -604,7 +604,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView): 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 fc04abe35d..df1bd93feb 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -308,7 +308,7 @@ class CycleViewSet(BaseViewSet): 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 @@ class TransferCycleIssueEndpoint(BaseAPIView): 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 211f5a88ac..6be2c9ea94 100644 --- a/apiserver/plane/app/views/cycle/issue.py +++ b/apiserver/plane/app/views/cycle/issue.py @@ -248,7 +248,7 @@ class CycleIssueViewSet(BaseViewSet): 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 4a760ca3b1..6a72fed289 100644 --- a/apiserver/plane/app/views/dashboard/base.py +++ b/apiserver/plane/app/views/dashboard/base.py @@ -40,8 +40,6 @@ from plane.db.models import ( 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 c5899d9725..0000000000 --- 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 0000000000..8ec09d1482 --- /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 5c173f2021..15e2d01da0 100644 --- a/apiserver/plane/app/views/workspace/user.py +++ b/apiserver/plane/app/views/workspace/user.py @@ -504,7 +504,7 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView): 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 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView): 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 8e648c16b0..e7ca16a984 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 0000000000..ee70f66152 --- /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 e7def641d5..a6fa6dddb7 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -5,6 +5,7 @@ from .base import BaseModel 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 b3ce49e01a..7c6ac8e393 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 0000000000..671b89ff1f --- /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 bcc1682273..3f784b399e 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.core.validators import MaxValueValidator, MinValueValidator 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 eda3b30ac9..fd9f640585 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 13/97] 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 20117d10f5..242ebfd0ce 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 14/97] [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 | 678 +++++++++++++----- 33 files changed, 1271 insertions(+), 189 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 e5047fb0c4..caffc0534a 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 0cb268fc88..fe72ef7aea 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 72e92a6a8a..0b14bd1357 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 0000000000..4dca9e6d53 --- /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 0000000000..fe6d89c0eb --- /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 0560002d84..c7cf53a5f5 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 6792ebf0ac..dafda1db3a 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}
) : ( <>