This commit is contained in:
DCCONSTRUCTIONS
2026-04-18 18:39:25 +03:00
commit 3ba092b60c
4944 changed files with 497564 additions and 0 deletions
@@ -0,0 +1,4 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
@@ -0,0 +1,4 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
@@ -0,0 +1,38 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
# Django imports
from django.core.management import BaseCommand, CommandError
# Module imports
from plane.db.models import User
class Command(BaseCommand):
help = "Make the user with the given email active"
def add_arguments(self, parser):
# Positional argument
parser.add_argument("email", type=str, help="user email")
def handle(self, *args, **options):
# get the user email from console
email = options.get("email", False)
# raise error if email is not present
if not email:
raise CommandError("Error: Email is required")
# filter the user
user = User.objects.filter(email=email).first()
# Raise error if the user is not present
if not user:
raise CommandError(f"Error: User with {email} does not exists")
# Activate the user
user.is_active = True
user.save()
self.stdout.write(self.style.SUCCESS("User activated successfully"))
@@ -0,0 +1,30 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
# Django imports
from django.core.cache import cache
from django.core.management import BaseCommand
class Command(BaseCommand):
help = "Clear Cache before starting the server to remove stale values"
def add_arguments(self, parser):
# Positional argument
parser.add_argument("--key", type=str, nargs="?", help="Key to clear cache")
def handle(self, *args, **options):
try:
if options["key"]:
cache.delete(options["key"])
self.stdout.write(self.style.SUCCESS(f"Cache Cleared for key: {options['key']}"))
return
cache.clear()
self.stdout.write(self.style.SUCCESS("Cache Cleared"))
return
except Exception:
# Another ClientError occurred
self.stdout.write(self.style.ERROR("Failed to clear cache"))
return
@@ -0,0 +1,53 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
# Django imports
from django.core.management.base import BaseCommand
from django.db import transaction
# Module imports
from plane.db.models import Description
from plane.db.models import IssueComment
class Command(BaseCommand):
help = "Create Description records for existing IssueComment"
def handle(self, *args, **kwargs):
batch_size = 500
while True:
comments = list(
IssueComment.objects.filter(description_id__isnull=True).order_by("created_at")[:batch_size]
)
if not comments:
break
with transaction.atomic():
descriptions = [
Description(
created_at=comment.created_at,
updated_at=comment.updated_at,
description_json=comment.comment_json,
description_html=comment.comment_html,
description_stripped=comment.comment_stripped,
project_id=comment.project_id,
created_by_id=comment.created_by_id,
updated_by_id=comment.updated_by_id,
workspace_id=comment.workspace_id,
)
for comment in comments
]
created_descriptions = Description.objects.bulk_create(descriptions)
comments_to_update = []
for comment, description in zip(comments, created_descriptions):
comment.description_id = description.id
comments_to_update.append(comment)
IssueComment.objects.bulk_update(comments_to_update, ["description_id"])
self.stdout.write(self.style.SUCCESS("Successfully Copied IssueComment to Description"))
@@ -0,0 +1,61 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
# Python imports
import os
import boto3
from botocore.exceptions import ClientError
# Django imports
from django.core.management import BaseCommand
class Command(BaseCommand):
help = "Create the default bucket for the instance"
def handle(self, *args, **options):
# Create a session using the credentials from Django settings
try:
s3_client = boto3.client(
"s3",
endpoint_url=os.environ.get("AWS_S3_ENDPOINT_URL"), # MinIO endpoint
aws_access_key_id=os.environ.get("AWS_ACCESS_KEY_ID"), # MinIO access key
aws_secret_access_key=os.environ.get("AWS_SECRET_ACCESS_KEY"), # MinIO secret key
region_name=os.environ.get("AWS_REGION"), # MinIO region
config=boto3.session.Config(signature_version="s3v4"),
)
# Get the bucket name from the environment
bucket_name = os.environ.get("AWS_S3_BUCKET_NAME")
self.stdout.write(self.style.NOTICE("Checking bucket..."))
# Check if the bucket exists
s3_client.head_bucket(Bucket=bucket_name)
# If the bucket exists, print a success message
self.stdout.write(self.style.SUCCESS(f"Bucket '{bucket_name}' exists."))
return
except ClientError as e:
error_code = int(e.response["Error"]["Code"])
bucket_name = os.environ.get("AWS_S3_BUCKET_NAME")
if error_code == 404:
# Bucket does not exist, create it
self.stdout.write(self.style.WARNING(f"Bucket '{bucket_name}' does not exist. Creating bucket..."))
try:
s3_client.create_bucket(Bucket=bucket_name)
self.stdout.write(self.style.SUCCESS(f"Bucket '{bucket_name}' created successfully."))
# Handle the exception if the bucket creation fails
except ClientError as create_error:
self.stdout.write(self.style.ERROR(f"Failed to create bucket: {create_error}"))
# Handle the exception if access to the bucket is forbidden
elif error_code == 403:
# Access to the bucket is forbidden
self.stdout.write(
self.style.ERROR(f"Access to the bucket '{bucket_name}' is forbidden. Check permissions.")
)
else:
# Another ClientError occurred
self.stdout.write(self.style.ERROR(f"Failed to check bucket: {e}"))
except Exception as ex:
# Handle any other exception
self.stdout.write(self.style.ERROR(f"An error occurred: {ex}"))
@@ -0,0 +1,74 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
# Django imports
from typing import Any
from django.core.management.base import BaseCommand, CommandError
# Module imports
from plane.db.models import User, Workspace, WorkspaceMember
class Command(BaseCommand):
help = "Create dump issues, cycles etc. for a project in a given workspace"
def handle(self, *args: Any, **options: Any) -> str | None:
try:
workspace_name = input("Workspace Name: ")
workspace_slug = input("Workspace slug: ")
if workspace_slug == "":
raise CommandError("Workspace slug is required")
if Workspace.objects.filter(slug=workspace_slug).exists():
raise CommandError("Workspace already exists")
creator = input("Your email: ")
if creator == "" or not User.objects.filter(email=creator).exists():
raise CommandError("User email is required and should have signed in plane")
user = User.objects.get(email=creator)
members = input("Enter Member emails (comma separated): ")
members = members.split(",") if members != "" else []
# Create workspace
workspace = Workspace.objects.create(slug=workspace_slug, name=workspace_name, owner=user)
# Create workspace member
WorkspaceMember.objects.create(workspace=workspace, role=20, member=user)
user_ids = User.objects.filter(email__in=members)
_ = WorkspaceMember.objects.bulk_create(
[WorkspaceMember(workspace=workspace, member=user_id, role=20) for user_id in user_ids],
ignore_conflicts=True,
)
project_count = int(input("Number of projects to be created: "))
for i in range(project_count):
print(f"Please provide the following details for project {i + 1}:")
issue_count = int(input("Number of issues to be created: "))
cycle_count = int(input("Number of cycles to be created: "))
module_count = int(input("Number of modules to be created: "))
pages_count = int(input("Number of pages to be created: "))
intake_issue_count = int(input("Number of intake issues to be created: "))
from plane.bgtasks.dummy_data_task import create_dummy_data
create_dummy_data(
slug=workspace_slug,
email=creator,
members=members,
issue_count=issue_count,
cycle_count=cycle_count,
module_count=module_count,
pages_count=pages_count,
intake_issue_count=intake_issue_count,
)
self.stdout.write(self.style.SUCCESS("Data is pushed to the queue"))
return
except Exception as e:
self.stdout.write(self.style.ERROR(f"Command errored out {str(e)}"))
return
@@ -0,0 +1,43 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
# Django imports
from django.core.management.base import BaseCommand, CommandError
# Module imports
from plane.license.models import Instance, InstanceAdmin
from plane.db.models import User
class Command(BaseCommand):
help = "Add a new instance admin"
def add_arguments(self, parser):
# Positional argument
parser.add_argument("admin_email", type=str, help="Instance Admin Email")
def handle(self, *args, **options):
admin_email = options.get("admin_email", False)
if not admin_email:
raise CommandError("Please provide the email of the admin.")
user = User.objects.filter(email=admin_email).first()
if user is None:
raise CommandError("User with the provided email does not exist.")
try:
# Get the instance
instance = Instance.objects.last()
# Get or create an instance admin
_, created = InstanceAdmin.objects.get_or_create(user=user, instance=instance, role=20)
if not created:
raise CommandError("The provided email is already an instance admin.")
self.stdout.write(self.style.SUCCESS("Successfully created the admin"))
except Exception as e:
print(e)
raise CommandError("Failed to create the instance admin.")
@@ -0,0 +1,72 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
# Django imports
from typing import Any
from django.core.management import BaseCommand, CommandError
# Module imports
from plane.db.models import (
User,
WorkspaceMember,
ProjectMember,
Project,
ProjectUserProperty,
)
class Command(BaseCommand):
help = "Add a member to a project. If present in the workspace"
def add_arguments(self, parser):
# Positional argument
parser.add_argument("--project_id", type=str, nargs="?", help="Project ID")
parser.add_argument("--user_email", type=str, nargs="?", help="User Email")
parser.add_argument("--role", type=int, nargs="?", help="Role of the user in the project")
def handle(self, *args: Any, **options: Any):
try:
if not options["project_id"]:
raise CommandError("Project ID is required")
if not options["user_email"]:
raise CommandError("User Email is required")
project_id = options["project_id"]
user_email = options["user_email"]
role = options.get("role", 20)
print(f"Role: {role}")
user = User.objects.filter(email=user_email).first()
if not user:
raise CommandError("User not found")
# Check if the project exists
project = Project.objects.filter(pk=project_id).first()
if not project:
raise CommandError("Project not found")
# Check if the user exists in the workspace
if not WorkspaceMember.objects.filter(workspace=project.workspace, member=user, is_active=True).exists():
raise CommandError("User not member in workspace")
if ProjectMember.objects.filter(project=project, member=user).exists():
# Update the project member
ProjectMember.objects.filter(project=project, member=user).update(
is_active=True, role=role
)
else:
# Create the project member
ProjectMember.objects.create(project=project, member=user, role=role)
# Issue Property
ProjectUserProperty.objects.get_or_create(user=user, project=project)
# Success message
self.stdout.write(self.style.SUCCESS(f"User {user_email} added to project {project_id}"))
return
except CommandError as e:
self.stdout.write(self.style.ERROR(e))
return
@@ -0,0 +1,95 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
# Django imports
from django.core.management.base import BaseCommand, CommandError
from django.db.models import Max
from django.db import connection, transaction
# Module imports
from plane.db.models import Project, Issue, IssueSequence
from plane.utils.uuid import convert_uuid_to_integer
class Command(BaseCommand):
help = "Fix duplicate sequences"
def add_arguments(self, parser):
# Positional argument
parser.add_argument("issue_identifier", type=str, help="Issue Identifier")
def strict_str_to_int(self, s):
if not s.isdigit() and not (s.startswith("-") and s[1:].isdigit()):
raise ValueError("Invalid integer string")
return int(s)
def handle(self, *args, **options):
workspace_slug = input("Workspace slug: ")
if not workspace_slug:
raise CommandError("Workspace slug is required")
issue_identifier = options.get("issue_identifier", False)
# Validate issue_identifier
if not issue_identifier:
raise CommandError("Issue identifier is required")
# Validate issue identifier
try:
identifier = issue_identifier.split("-")
if len(identifier) != 2:
raise ValueError("Invalid issue identifier format")
project_identifier = identifier[0]
issue_sequence = self.strict_str_to_int(identifier[1])
# Fetch the project
project = Project.objects.get(identifier__iexact=project_identifier, workspace__slug=workspace_slug)
# Get the issues
issues = Issue.objects.filter(project=project, sequence_id=issue_sequence)
# Check if there are duplicate issues
if not issues.count() > 1:
raise CommandError("No duplicate issues found with the given identifier")
self.stdout.write(self.style.SUCCESS(f"{issues.count()} issues found with identifier {issue_identifier}"))
with transaction.atomic():
# This ensures only one transaction per project can execute this code at a time
lock_key = convert_uuid_to_integer(project.id)
# Acquire an exclusive lock using the project ID as the lock key
with connection.cursor() as cursor:
# Get an exclusive lock using the project ID as the lock key
cursor.execute("SELECT pg_advisory_xact_lock(%s)", [lock_key])
# Get the maximum sequence ID for the project
last_sequence = IssueSequence.objects.filter(project=project).aggregate(largest=Max("sequence"))[
"largest"
]
bulk_issues = []
bulk_issue_sequences = []
issue_sequence_map = {isq.issue_id: isq for isq in IssueSequence.objects.filter(project=project)}
# change the ids of duplicate issues
for index, issue in enumerate(issues[1:]):
updated_sequence_id = last_sequence + index + 1
issue.sequence_id = updated_sequence_id
bulk_issues.append(issue)
# Find the same issue sequence instance from the above queryset
sequence_identifier = issue_sequence_map.get(issue.id)
if sequence_identifier:
sequence_identifier.sequence = updated_sequence_id
bulk_issue_sequences.append(sequence_identifier)
Issue.objects.bulk_update(bulk_issues, ["sequence_id"])
IssueSequence.objects.bulk_update(bulk_issue_sequences, ["sequence"])
self.stdout.write(self.style.SUCCESS("Sequence IDs updated successfully"))
except Exception as e:
raise CommandError(str(e))
@@ -0,0 +1,64 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
# Python imports
import getpass
# Django imports
from django.core.management import BaseCommand, CommandError
# Module imports
from plane.authentication.utils.password_validation import is_password_policy_valid
from plane.db.models import User
class Command(BaseCommand):
help = "Reset password of the user with the given email"
def add_arguments(self, parser):
# Positional argument
parser.add_argument("email", type=str, help="user email")
def handle(self, *args, **options):
# get the user email from console
email = options.get("email", False)
# raise error if email is not present
if not email:
self.stderr.write("Error: Email is required")
return
# filter the user
user = User.objects.filter(email=email).first()
# Raise error if the user is not present
if not user:
self.stderr.write(f"Error: User with {email} does not exists")
return
# get password for the user
password = getpass.getpass("Password: ")
confirm_password = getpass.getpass("Password (again): ")
# If the passwords doesn't match raise error
if password != confirm_password:
self.stderr.write("Error: Your passwords didn't match.")
return
# Blank passwords should not be allowed
if password.strip() == "":
self.stderr.write("Error: Blank passwords aren't allowed.")
return
if not is_password_policy_valid(password):
raise CommandError(
"Password must be at least 8 characters long and include upper-case, lower-case, number, special character, and must not be predictable"
)
# Set user password
user.set_password(password)
user.is_password_autoset = False
user.save()
self.stdout.write(self.style.SUCCESS("User password updated successfully"))
@@ -0,0 +1,23 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
# Django imports
from django.core.management.base import BaseCommand
# Module imports
from plane.bgtasks.issue_description_version_sync import (
schedule_issue_description_version,
)
class Command(BaseCommand):
help = "Creates IssueDescriptionVersion records for existing Issues in batches"
def handle(self, *args, **options):
batch_size = input("Enter the batch size: ")
batch_countdown = input("Enter the batch countdown: ")
schedule_issue_description_version.delay(batch_size=batch_size, countdown=int(batch_countdown))
self.stdout.write(self.style.SUCCESS("Successfully created issue description version task"))
@@ -0,0 +1,21 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
# Django imports
from django.core.management.base import BaseCommand
# Module imports
from plane.bgtasks.issue_version_sync import schedule_issue_version
class Command(BaseCommand):
help = "Creates IssueVersion records for existing Issues in batches"
def handle(self, *args, **options):
batch_size = input("Enter the batch size: ")
batch_countdown = input("Enter the batch countdown: ")
schedule_issue_version.delay(batch_size=batch_size, countdown=int(batch_countdown))
self.stdout.write(self.style.SUCCESS("Successfully created issue version task"))
@@ -0,0 +1,67 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
from django.core.mail import EmailMultiAlternatives, get_connection
from django.core.management import BaseCommand, CommandError
from django.template.loader import render_to_string
from django.utils.html import strip_tags
# Module imports
from plane.license.utils.instance_value import get_email_configuration
class Command(BaseCommand):
"""Django command to pause execution until db is available"""
def add_arguments(self, parser):
# Positional argument
parser.add_argument("to_email", type=str, help="receiver's email")
def handle(self, *args, **options):
receiver_email = options.get("to_email")
if not receiver_email:
raise CommandError("Receiver email is required")
(
EMAIL_HOST,
EMAIL_HOST_USER,
EMAIL_HOST_PASSWORD,
EMAIL_PORT,
EMAIL_USE_TLS,
EMAIL_USE_SSL,
EMAIL_FROM,
) = get_email_configuration()
connection = get_connection(
host=EMAIL_HOST,
port=int(EMAIL_PORT),
username=EMAIL_HOST_USER,
password=EMAIL_HOST_PASSWORD,
use_tls=EMAIL_USE_TLS == "1",
use_ssl=EMAIL_USE_SSL == "1",
timeout=30,
)
# Prepare email details
subject = "Test email from Plane"
html_content = render_to_string("emails/test_email.html")
text_content = strip_tags(html_content)
self.stdout.write(self.style.SUCCESS("Trying to send test email..."))
# Send the email
try:
msg = EmailMultiAlternatives(
subject=subject,
body=text_content,
from_email=EMAIL_FROM,
to=[receiver_email],
connection=connection,
)
msg.attach_alternative(html_content, "text/html")
msg.send()
self.stdout.write(self.style.SUCCESS("Email successfully sent"))
except Exception as e:
self.stdout.write(self.style.ERROR(f"Error: Email could not be delivered due to {e}"))
@@ -0,0 +1,187 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
# Python imports
import os
import boto3
from botocore.exceptions import ClientError
import json
# Django imports
from django.core.management import BaseCommand
class Command(BaseCommand):
help = "Create the default bucket for the instance"
def get_s3_client(self):
s3_client = boto3.client(
"s3",
endpoint_url=os.environ.get("AWS_S3_ENDPOINT_URL"), # MinIO endpoint
aws_access_key_id=os.environ.get("AWS_ACCESS_KEY_ID"), # MinIO access key
aws_secret_access_key=os.environ.get("AWS_SECRET_ACCESS_KEY"), # MinIO secret key
region_name=os.environ.get("AWS_REGION"), # MinIO region
config=boto3.session.Config(signature_version="s3v4"),
)
return s3_client
# Check if the access key has the required permissions
def check_s3_permissions(self, bucket_name):
s3_client = self.get_s3_client()
permissions = {
"s3:GetObject": False,
"s3:ListBucket": False,
"s3:PutBucketPolicy": False,
"s3:PutObject": False,
}
# 1. Test s3:ListBucket (attempt to list the bucket contents)
try:
s3_client.list_objects_v2(Bucket=bucket_name)
permissions["s3:ListBucket"] = True
except ClientError as e:
if e.response["Error"]["Code"] == "AccessDenied":
self.stdout.write("ListBucket permission denied.")
else:
self.stdout.write(f"Error in ListBucket: {e}")
# 2. Test s3:GetObject (attempt to get a specific object)
try:
response = s3_client.list_objects_v2(Bucket=bucket_name)
if "Contents" in response:
test_object_key = response["Contents"][0]["Key"]
s3_client.get_object(Bucket=bucket_name, Key=test_object_key)
permissions["s3:GetObject"] = True
except ClientError as e:
if e.response["Error"]["Code"] == "AccessDenied":
self.stdout.write("GetObject permission denied.")
else:
self.stdout.write(f"Error in GetObject: {e}")
# 3. Test s3:PutObject (attempt to upload an object)
try:
s3_client.put_object(Bucket=bucket_name, Key="test_permission_check.txt", Body=b"Test")
permissions["s3:PutObject"] = True
# Clean up
except ClientError as e:
if e.response["Error"]["Code"] == "AccessDenied":
self.stdout.write("PutObject permission denied.")
else:
self.stdout.write(f"Error in PutObject: {e}")
# Clean up
try:
s3_client.delete_object(Bucket=bucket_name, Key="test_permission_check.txt")
except ClientError:
self.stdout.write("Couldn't delete test object")
# 4. Test s3:PutBucketPolicy (attempt to put a bucket policy)
try:
policy = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": f"arn:aws:s3:::{bucket_name}/*",
}
],
}
s3_client.put_bucket_policy(Bucket=bucket_name, Policy=json.dumps(policy))
permissions["s3:PutBucketPolicy"] = True
except ClientError as e:
if e.response["Error"]["Code"] == "AccessDenied":
self.stdout.write("PutBucketPolicy permission denied.")
else:
self.stdout.write(f"Error in PutBucketPolicy: {e}")
return permissions
def generate_bucket_policy(self, bucket_name):
s3_client = self.get_s3_client()
response = s3_client.list_objects_v2(Bucket=bucket_name)
public_object_resource = []
if "Contents" in response:
for obj in response["Contents"]:
object_key = obj["Key"]
public_object_resource.append(f"arn:aws:s3:::{bucket_name}/{object_key}")
bucket_policy = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": public_object_resource,
}
],
}
return bucket_policy
def make_objects_public(self, bucket_name):
# Initialize S3 client
s3_client = self.get_s3_client()
# Get the bucket policy
bucket_policy = self.generate_bucket_policy(bucket_name)
# Apply the policy to the bucket
s3_client.put_bucket_policy(Bucket=bucket_name, Policy=json.dumps(bucket_policy))
# Print a success message
self.stdout.write("Bucket is private, but existing objects remain public.")
return
def handle(self, *args, **options):
# Create a session using the credentials from Django settings
# Check if the bucket exists
s3_client = self.get_s3_client()
# Get the bucket name from the environment
bucket_name = os.environ.get("AWS_S3_BUCKET_NAME")
if not bucket_name:
self.stdout.write(self.style.ERROR("Please set the AWS_S3_BUCKET_NAME environment variable."))
return
self.stdout.write(self.style.NOTICE("Checking bucket..."))
# Check if the bucket exists
try:
s3_client.head_bucket(Bucket=bucket_name)
except ClientError as e:
error_code = e.response["Error"]["Code"]
if error_code == "404":
self.stdout.write(self.style.ERROR(f"Bucket '{bucket_name}' does not exist."))
return
else:
self.stdout.write(f"Error: {e}")
# If the bucket exists, print a success message
self.stdout.write(self.style.SUCCESS(f"Bucket '{bucket_name}' exists."))
try:
# Check the permissions of the access key
permissions = self.check_s3_permissions(bucket_name)
except ClientError as e:
self.stdout.write(f"Error: {e}")
except Exception as e:
self.stdout.write(f"Error: {e}")
# If the access key has the required permissions
try:
if all(permissions.values()):
self.stdout.write(self.style.SUCCESS("Access key has the required permissions."))
# Making the existing objects public
self.make_objects_public(bucket_name)
return
except Exception as e:
self.stdout.write(f"Error: {e}")
# write the bucket policy to a file
self.stdout.write(self.style.WARNING("Generating permissions.json for manual bucket policy update."))
try:
# Writing to a file
with open("permissions.json", "w") as f:
f.write(json.dumps(self.generate_bucket_policy(bucket_name)))
self.stdout.write(self.style.WARNING("Permissions have been written to permissions.json."))
return
except IOError as e:
self.stdout.write(f"Error writing permissions.json: {e}")
return
@@ -0,0 +1,71 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
from django.core.management.base import BaseCommand
from django.db import transaction
from plane.db.models import Workspace
class Command(BaseCommand):
help = "Updates the slug of a soft-deleted workspace by appending the epoch timestamp"
def add_arguments(self, parser):
parser.add_argument(
"slug",
type=str,
help="The slug of the workspace to update",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Run the command without making any changes",
)
def handle(self, *args, **options):
slug = options["slug"]
dry_run = options["dry_run"]
# Get the workspace with the specified slug
try:
workspace = Workspace.all_objects.get(slug=slug)
except Workspace.DoesNotExist:
self.stdout.write(self.style.ERROR(f"Workspace with slug '{slug}' not found."))
return
# Check if the workspace is soft-deleted
if workspace.deleted_at is None:
self.stdout.write(
self.style.WARNING(f"Workspace '{workspace.name}' (slug: {workspace.slug}) is not deleted.")
)
return
# Check if the slug already has a timestamp appended
if "__" in workspace.slug and workspace.slug.split("__")[-1].isdigit():
self.stdout.write(
self.style.WARNING(
f"Workspace '{workspace.name}' (slug: {workspace.slug}) already has a timestamp appended."
)
)
return
# Get the deletion timestamp
deletion_timestamp = int(workspace.deleted_at.timestamp())
# Create the new slug with the deletion timestamp
new_slug = f"{workspace.slug}__{deletion_timestamp}"
if dry_run:
self.stdout.write(f"Would update workspace '{workspace.name}' slug from '{workspace.slug}' to '{new_slug}'")
else:
try:
with transaction.atomic():
workspace.slug = new_slug
workspace.save(update_fields=["slug"])
self.stdout.write(
self.style.SUCCESS(
f"Updated workspace '{workspace.name}' slug from '{workspace.slug}' to '{new_slug}'"
)
)
except Exception as e:
self.stdout.write(self.style.ERROR(f"Error updating workspace '{workspace.name}': {str(e)}"))
@@ -0,0 +1,24 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
import time
from django.db import connections
from django.db.utils import OperationalError
from django.core.management import BaseCommand
class Command(BaseCommand):
"""Django command to pause execution until db is available"""
def handle(self, *args, **options):
self.stdout.write("Waiting for database...")
db_conn = None
while not db_conn:
try:
db_conn = connections["default"]
except OperationalError:
self.stdout.write("Database unavailable, waititng 1 second...")
time.sleep(1)
self.stdout.write(self.style.SUCCESS("Database available!"))
@@ -0,0 +1,26 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
# wait_for_migrations.py
import time
from django.core.management.base import BaseCommand
from django.db.migrations.executor import MigrationExecutor
from django.db import connections, DEFAULT_DB_ALIAS
class Command(BaseCommand):
help = "Wait for database migrations to complete before starting Celery worker/beat"
def handle(self, *args, **kwargs):
while self._pending_migrations():
self.stdout.write("Waiting for database migrations to complete...")
time.sleep(10) # wait for 10 seconds before checking again
self.stdout.write(self.style.SUCCESS("No migrations Pending. Starting processes ..."))
def _pending_migrations(self):
connection = connections[DEFAULT_DB_ALIAS]
executor = MigrationExecutor(connection)
targets = executor.loader.graph.leaf_nodes()
return bool(executor.migration_plan(targets))