base
This commit is contained in:
@@ -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,471 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
"""Global Settings"""
|
||||
|
||||
# Python imports
|
||||
import os
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import urljoin
|
||||
|
||||
# Third party imports
|
||||
import dj_database_url
|
||||
|
||||
# Django imports
|
||||
from django.core.management.utils import get_random_secret_key
|
||||
from corsheaders.defaults import default_headers
|
||||
|
||||
|
||||
# Module imports
|
||||
from plane.utils.url import is_valid_url
|
||||
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Secret Key
|
||||
SECRET_KEY = os.environ.get("SECRET_KEY", get_random_secret_key())
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = int(os.environ.get("DEBUG", "0"))
|
||||
|
||||
# Self-hosted mode
|
||||
IS_SELF_MANAGED = True
|
||||
|
||||
# Allowed Hosts
|
||||
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "*").split(",")
|
||||
|
||||
# Application definition
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.staticfiles",
|
||||
# Inhouse apps
|
||||
"plane.analytics",
|
||||
"plane.app",
|
||||
"plane.space",
|
||||
"plane.bgtasks",
|
||||
"plane.db",
|
||||
"plane.utils",
|
||||
"plane.web",
|
||||
"plane.middleware",
|
||||
"plane.license",
|
||||
"plane.api",
|
||||
"plane.authentication",
|
||||
# Third-party things
|
||||
"rest_framework",
|
||||
"corsheaders",
|
||||
"django_celery_beat",
|
||||
]
|
||||
|
||||
# Middlewares
|
||||
MIDDLEWARE = [
|
||||
"corsheaders.middleware.CorsMiddleware",
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||
"plane.authentication.middleware.session.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"crum.CurrentRequestUserMiddleware",
|
||||
"django.middleware.gzip.GZipMiddleware",
|
||||
"plane.middleware.request_body_size.RequestBodySizeLimitMiddleware",
|
||||
"plane.middleware.logger.APITokenLogMiddleware",
|
||||
"plane.middleware.logger.RequestLoggerMiddleware",
|
||||
]
|
||||
|
||||
# Rest Framework settings
|
||||
REST_FRAMEWORK = {
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework.authentication.SessionAuthentication",),
|
||||
"DEFAULT_THROTTLE_CLASSES": ("rest_framework.throttling.AnonRateThrottle",),
|
||||
"DEFAULT_THROTTLE_RATES": {
|
||||
"anon": "30/minute",
|
||||
"asset_id": "5/minute",
|
||||
},
|
||||
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
|
||||
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
|
||||
"DEFAULT_FILTER_BACKENDS": ("django_filters.rest_framework.DjangoFilterBackend",),
|
||||
"EXCEPTION_HANDLER": "plane.authentication.adapter.exception.auth_exception_handler",
|
||||
# Preserve original Django URL parameter names (pk) instead of converting to 'id'
|
||||
"SCHEMA_COERCE_PATH_PK": False,
|
||||
}
|
||||
|
||||
# Django Auth Backend
|
||||
AUTHENTICATION_BACKENDS = ("django.contrib.auth.backends.ModelBackend",) # default
|
||||
|
||||
# Root Urls
|
||||
ROOT_URLCONF = "plane.urls"
|
||||
|
||||
# Templates
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": ["templates"],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
]
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
# CORS Settings
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
cors_origins_raw = os.environ.get("CORS_ALLOWED_ORIGINS", "")
|
||||
# filter out empty strings
|
||||
cors_allowed_origins = [origin.strip() for origin in cors_origins_raw.split(",") if origin.strip()]
|
||||
if cors_allowed_origins:
|
||||
CORS_ALLOWED_ORIGINS = cors_allowed_origins
|
||||
secure_origins = False if [origin for origin in cors_allowed_origins if "http:" in origin] else True
|
||||
else:
|
||||
CORS_ALLOW_ALL_ORIGINS = True
|
||||
secure_origins = False
|
||||
|
||||
CORS_ALLOW_HEADERS = [*default_headers, "X-API-Key"]
|
||||
|
||||
# Application Settings
|
||||
WSGI_APPLICATION = "plane.wsgi.application"
|
||||
ASGI_APPLICATION = "plane.asgi.application"
|
||||
|
||||
# Django Sites
|
||||
SITE_ID = 1
|
||||
|
||||
# User Model
|
||||
AUTH_USER_MODEL = "db.User"
|
||||
|
||||
# Database
|
||||
if bool(os.environ.get("DATABASE_URL")):
|
||||
# Parse database configuration from $DATABASE_URL
|
||||
DATABASES = {"default": dj_database_url.config()}
|
||||
else:
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"NAME": os.environ.get("POSTGRES_DB"),
|
||||
"USER": os.environ.get("POSTGRES_USER"),
|
||||
"PASSWORD": os.environ.get("POSTGRES_PASSWORD"),
|
||||
"HOST": os.environ.get("POSTGRES_HOST"),
|
||||
"PORT": os.environ.get("POSTGRES_PORT", "5432"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if os.environ.get("ENABLE_READ_REPLICA", "0") == "1":
|
||||
if bool(os.environ.get("DATABASE_READ_REPLICA_URL")):
|
||||
# Parse database configuration from $DATABASE_URL
|
||||
DATABASES["replica"] = dj_database_url.parse(os.environ.get("DATABASE_READ_REPLICA_URL"))
|
||||
else:
|
||||
DATABASES["replica"] = {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"NAME": os.environ.get("POSTGRES_READ_REPLICA_DB"),
|
||||
"USER": os.environ.get("POSTGRES_READ_REPLICA_USER"),
|
||||
"PASSWORD": os.environ.get("POSTGRES_READ_REPLICA_PASSWORD"),
|
||||
"HOST": os.environ.get("POSTGRES_READ_REPLICA_HOST"),
|
||||
"PORT": os.environ.get("POSTGRES_READ_REPLICA_PORT", "5432"),
|
||||
}
|
||||
|
||||
# Database Routers
|
||||
DATABASE_ROUTERS = ["plane.utils.core.dbrouters.ReadReplicaRouter"]
|
||||
# Add middleware at the end for read replica routing
|
||||
MIDDLEWARE.append("plane.middleware.db_routing.ReadReplicaRoutingMiddleware")
|
||||
|
||||
|
||||
# Redis Config
|
||||
REDIS_URL = os.environ.get("REDIS_URL")
|
||||
REDIS_SSL = REDIS_URL and "rediss" in REDIS_URL
|
||||
|
||||
if REDIS_SSL:
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": REDIS_URL,
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
"CONNECTION_POOL_KWARGS": {"ssl_cert_reqs": False},
|
||||
},
|
||||
}
|
||||
}
|
||||
else:
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": REDIS_URL,
|
||||
"OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"},
|
||||
}
|
||||
}
|
||||
|
||||
# Password validations
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
|
||||
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
|
||||
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
|
||||
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
|
||||
]
|
||||
|
||||
# Password reset time the number of seconds the uniquely generated uid will be valid
|
||||
PASSWORD_RESET_TIMEOUT = 3600
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
STATIC_URL = "/static/"
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, "static-assets", "collected-static")
|
||||
STATICFILES_DIRS = (os.path.join(BASE_DIR, "static"),)
|
||||
|
||||
# Media Settings
|
||||
MEDIA_ROOT = "mediafiles"
|
||||
MEDIA_URL = "/media/"
|
||||
|
||||
# Internationalization
|
||||
LANGUAGE_CODE = "en-us"
|
||||
USE_I18N = True
|
||||
USE_L10N = True
|
||||
|
||||
# Timezones
|
||||
USE_TZ = True
|
||||
TIME_ZONE = "UTC"
|
||||
|
||||
# Default Auto Field
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
# Email settings
|
||||
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
|
||||
|
||||
# Storage Settings
|
||||
# Use Minio settings
|
||||
USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1
|
||||
|
||||
STORAGES = {"staticfiles": {"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage"}}
|
||||
STORAGES["default"] = {"BACKEND": "plane.settings.storage.S3Storage"}
|
||||
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "access-key")
|
||||
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "secret-key")
|
||||
AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "uploads")
|
||||
AWS_REGION = os.environ.get("AWS_REGION", "")
|
||||
AWS_DEFAULT_ACL = "public-read"
|
||||
AWS_QUERYSTRING_AUTH = False
|
||||
AWS_S3_FILE_OVERWRITE = False
|
||||
AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", None) or os.environ.get("MINIO_ENDPOINT_URL", None)
|
||||
if AWS_S3_ENDPOINT_URL and USE_MINIO:
|
||||
parsed_url = urlparse(os.environ.get("WEB_URL", "http://localhost"))
|
||||
AWS_S3_CUSTOM_DOMAIN = f"{parsed_url.netloc}/{AWS_STORAGE_BUCKET_NAME}"
|
||||
AWS_S3_URL_PROTOCOL = f"{parsed_url.scheme}:"
|
||||
|
||||
# RabbitMQ connection settings
|
||||
RABBITMQ_HOST = os.environ.get("RABBITMQ_HOST", "localhost")
|
||||
RABBITMQ_PORT = os.environ.get("RABBITMQ_PORT", "5672")
|
||||
RABBITMQ_USER = os.environ.get("RABBITMQ_USER", "guest")
|
||||
RABBITMQ_PASSWORD = os.environ.get("RABBITMQ_PASSWORD", "guest")
|
||||
RABBITMQ_VHOST = os.environ.get("RABBITMQ_VHOST", "/")
|
||||
AMQP_URL = os.environ.get("AMQP_URL")
|
||||
|
||||
# Celery Configuration
|
||||
if AMQP_URL:
|
||||
CELERY_BROKER_URL = AMQP_URL
|
||||
else:
|
||||
CELERY_BROKER_URL = f"amqp://{RABBITMQ_USER}:{RABBITMQ_PASSWORD}@{RABBITMQ_HOST}:{RABBITMQ_PORT}/{RABBITMQ_VHOST}"
|
||||
|
||||
CELERY_TIMEZONE = TIME_ZONE
|
||||
CELERY_TASK_SERIALIZER = "json"
|
||||
CELERY_RESULT_SERIALIZER = "json"
|
||||
CELERY_ACCEPT_CONTENT = ["application/json"]
|
||||
|
||||
|
||||
CELERY_IMPORTS = (
|
||||
# scheduled tasks
|
||||
"plane.bgtasks.issue_automation_task",
|
||||
"plane.bgtasks.exporter_expired_task",
|
||||
"plane.bgtasks.file_asset_task",
|
||||
"plane.bgtasks.email_notification_task",
|
||||
"plane.bgtasks.cleanup_task",
|
||||
"plane.license.bgtasks.tracer",
|
||||
# management tasks
|
||||
"plane.bgtasks.dummy_data_task",
|
||||
# issue version tasks
|
||||
"plane.bgtasks.issue_version_sync",
|
||||
"plane.bgtasks.issue_description_version_sync",
|
||||
)
|
||||
|
||||
FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880))
|
||||
|
||||
# Unsplash Access key
|
||||
UNSPLASH_ACCESS_KEY = os.environ.get("UNSPLASH_ACCESS_KEY")
|
||||
# Github Access Token
|
||||
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
|
||||
|
||||
# Analytics
|
||||
ANALYTICS_SECRET_KEY = os.environ.get("ANALYTICS_SECRET_KEY", False)
|
||||
ANALYTICS_BASE_API = os.environ.get("ANALYTICS_BASE_API", False)
|
||||
|
||||
# Posthog settings
|
||||
POSTHOG_API_KEY = os.environ.get("POSTHOG_API_KEY", False)
|
||||
POSTHOG_HOST = os.environ.get("POSTHOG_HOST", False)
|
||||
|
||||
# Skip environment variable configuration
|
||||
SKIP_ENV_VAR = os.environ.get("SKIP_ENV_VAR", "1") == "1"
|
||||
|
||||
DATA_UPLOAD_MAX_MEMORY_SIZE = int(os.environ.get("FILE_SIZE_LIMIT", 5242880))
|
||||
|
||||
# Cookie Settings
|
||||
SESSION_COOKIE_SECURE = secure_origins
|
||||
SESSION_COOKIE_HTTPONLY = True
|
||||
SESSION_ENGINE = "plane.db.models.session"
|
||||
SESSION_COOKIE_AGE = int(os.environ.get("SESSION_COOKIE_AGE", 604800))
|
||||
SESSION_COOKIE_NAME = os.environ.get("SESSION_COOKIE_NAME", "session-id")
|
||||
SESSION_COOKIE_DOMAIN = os.environ.get("COOKIE_DOMAIN", None)
|
||||
SESSION_SAVE_EVERY_REQUEST = os.environ.get("SESSION_SAVE_EVERY_REQUEST", "0") == "1"
|
||||
|
||||
# Admin Cookie
|
||||
ADMIN_SESSION_COOKIE_NAME = "admin-session-id"
|
||||
ADMIN_SESSION_COOKIE_AGE = int(os.environ.get("ADMIN_SESSION_COOKIE_AGE", 3600))
|
||||
|
||||
# CSRF cookies
|
||||
CSRF_COOKIE_SECURE = secure_origins
|
||||
CSRF_COOKIE_HTTPONLY = True
|
||||
CSRF_TRUSTED_ORIGINS = cors_allowed_origins
|
||||
CSRF_COOKIE_DOMAIN = os.environ.get("COOKIE_DOMAIN", None)
|
||||
CSRF_FAILURE_VIEW = "plane.authentication.views.common.csrf_failure"
|
||||
|
||||
###### Base URLs ######
|
||||
|
||||
# Admin Base URL
|
||||
ADMIN_BASE_URL = os.environ.get("ADMIN_BASE_URL", None)
|
||||
if ADMIN_BASE_URL and not is_valid_url(ADMIN_BASE_URL):
|
||||
ADMIN_BASE_URL = None
|
||||
ADMIN_BASE_PATH = os.environ.get("ADMIN_BASE_PATH", "/god-mode/")
|
||||
|
||||
# Space Base URL
|
||||
SPACE_BASE_URL = os.environ.get("SPACE_BASE_URL", None)
|
||||
if SPACE_BASE_URL and not is_valid_url(SPACE_BASE_URL):
|
||||
SPACE_BASE_URL = None
|
||||
SPACE_BASE_PATH = os.environ.get("SPACE_BASE_PATH", "/spaces/")
|
||||
|
||||
# App Base URL
|
||||
APP_BASE_URL = os.environ.get("APP_BASE_URL", None)
|
||||
if APP_BASE_URL and not is_valid_url(APP_BASE_URL):
|
||||
APP_BASE_URL = None
|
||||
APP_BASE_PATH = os.environ.get("APP_BASE_PATH", "/")
|
||||
|
||||
# Live Base URL
|
||||
LIVE_BASE_URL = os.environ.get("LIVE_BASE_URL", None)
|
||||
if LIVE_BASE_URL and not is_valid_url(LIVE_BASE_URL):
|
||||
LIVE_BASE_URL = None
|
||||
LIVE_BASE_PATH = os.environ.get("LIVE_BASE_PATH", "/live/")
|
||||
|
||||
LIVE_URL = urljoin(LIVE_BASE_URL, LIVE_BASE_PATH) if LIVE_BASE_URL else None
|
||||
|
||||
# WEB URL
|
||||
WEB_URL = os.environ.get("WEB_URL")
|
||||
|
||||
HARD_DELETE_AFTER_DAYS = int(os.environ.get("HARD_DELETE_AFTER_DAYS", 60))
|
||||
|
||||
# Instance Changelog URL
|
||||
INSTANCE_CHANGELOG_URL = os.environ.get("INSTANCE_CHANGELOG_URL", "")
|
||||
|
||||
ATTACHMENT_MIME_TYPES = [
|
||||
# Images
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
"image/svg+xml",
|
||||
"image/webp",
|
||||
"image/tiff",
|
||||
"image/bmp",
|
||||
# Documents
|
||||
"application/pdf",
|
||||
"application/msword",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/vnd.ms-excel",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"application/vnd.ms-powerpoint",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
"text/plain",
|
||||
"text/markdown",
|
||||
"application/rtf",
|
||||
"application/vnd.oasis.opendocument.spreadsheet",
|
||||
"application/vnd.oasis.opendocument.text",
|
||||
"application/vnd.oasis.opendocument.presentation",
|
||||
"application/vnd.oasis.opendocument.graphics",
|
||||
# Microsoft Visio
|
||||
"application/vnd.visio",
|
||||
# Netpbm format
|
||||
"image/x-portable-graymap",
|
||||
"image/x-portable-bitmap",
|
||||
"image/x-portable-pixmap",
|
||||
# Open Office Bae
|
||||
"application/vnd.oasis.opendocument.database",
|
||||
# Audio
|
||||
"audio/mpeg",
|
||||
"audio/wav",
|
||||
"audio/ogg",
|
||||
"audio/midi",
|
||||
"audio/x-midi",
|
||||
"audio/aac",
|
||||
"audio/flac",
|
||||
"audio/x-m4a",
|
||||
# Video
|
||||
"video/mp4",
|
||||
"video/mpeg",
|
||||
"video/ogg",
|
||||
"video/webm",
|
||||
"video/quicktime",
|
||||
"video/x-msvideo",
|
||||
"video/x-ms-wmv",
|
||||
# Archives
|
||||
"application/zip",
|
||||
"application/x-rar",
|
||||
"application/x-rar-compressed",
|
||||
"application/x-tar",
|
||||
"application/gzip",
|
||||
"application/x-zip",
|
||||
"application/x-zip-compressed",
|
||||
"application/x-7z-compressed",
|
||||
"application/x-compressed",
|
||||
"application/x-compressed-tar",
|
||||
"application/x-compressed-tar-gz",
|
||||
"application/x-compressed-tar-bz2",
|
||||
"application/x-compressed-tar-zip",
|
||||
"application/x-compressed-tar-7z",
|
||||
"application/x-compressed-tar-rar",
|
||||
"application/x-compressed-tar-zip",
|
||||
# 3D Models
|
||||
"model/gltf-binary",
|
||||
"model/gltf+json",
|
||||
"application/octet-stream", # for .obj files, but be cautious
|
||||
# Fonts
|
||||
"font/ttf",
|
||||
"font/otf",
|
||||
"font/woff",
|
||||
"font/woff2",
|
||||
# Other
|
||||
"text/css",
|
||||
"text/javascript",
|
||||
"application/json",
|
||||
"text/xml",
|
||||
"text/csv",
|
||||
"application/xml",
|
||||
# SQL
|
||||
"application/x-sql",
|
||||
# Gzip
|
||||
"application/x-gzip",
|
||||
# Markdown
|
||||
"text/markdown",
|
||||
]
|
||||
|
||||
# Seed directory path
|
||||
SEED_DIR = os.path.join(BASE_DIR, "seeds")
|
||||
|
||||
ENABLE_DRF_SPECTACULAR = os.environ.get("ENABLE_DRF_SPECTACULAR", "0") == "1"
|
||||
|
||||
if ENABLE_DRF_SPECTACULAR:
|
||||
REST_FRAMEWORK["DEFAULT_SCHEMA_CLASS"] = "drf_spectacular.openapi.AutoSchema"
|
||||
INSTALLED_APPS.append("drf_spectacular")
|
||||
from .openapi import SPECTACULAR_SETTINGS # noqa: F401
|
||||
|
||||
# MongoDB Settings
|
||||
MONGO_DB_URL = os.environ.get("MONGO_DB_URL", False)
|
||||
MONGO_DB_DATABASE = os.environ.get("MONGO_DB_DATABASE", False)
|
||||
@@ -0,0 +1,94 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
"""Development settings"""
|
||||
|
||||
import os
|
||||
|
||||
from .common import * # noqa
|
||||
|
||||
DEBUG = True
|
||||
|
||||
# Debug Toolbar settings
|
||||
INSTALLED_APPS += ("debug_toolbar",) # noqa
|
||||
MIDDLEWARE += ("debug_toolbar.middleware.DebugToolbarMiddleware",) # noqa
|
||||
|
||||
DEBUG_TOOLBAR_PATCH_SETTINGS = False
|
||||
|
||||
# Only show emails in console don't send it to smtp
|
||||
EMAIL_BACKEND = os.environ.get("EMAIL_BACKEND", "django.core.mail.backends.console.EmailBackend")
|
||||
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": REDIS_URL, # noqa
|
||||
"OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"},
|
||||
}
|
||||
}
|
||||
|
||||
INTERNAL_IPS = ("127.0.0.1",)
|
||||
|
||||
MEDIA_URL = "/uploads/"
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, "uploads") # noqa
|
||||
|
||||
LOG_DIR = os.path.join(BASE_DIR, "logs") # noqa
|
||||
|
||||
if not os.path.exists(LOG_DIR):
|
||||
os.makedirs(LOG_DIR)
|
||||
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": True,
|
||||
"formatters": {
|
||||
"verbose": {
|
||||
"format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}",
|
||||
"style": "{",
|
||||
},
|
||||
"json": {
|
||||
"()": "pythonjsonlogger.jsonlogger.JsonFormatter",
|
||||
"fmt": "%(levelname)s %(asctime)s %(module)s %(name)s %(message)s",
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"level": "DEBUG",
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "json",
|
||||
}
|
||||
},
|
||||
"loggers": {
|
||||
"plane.api.request": {
|
||||
"level": "INFO",
|
||||
"handlers": ["console"],
|
||||
"propagate": False,
|
||||
},
|
||||
"plane.api": {"level": "INFO", "handlers": ["console"], "propagate": False},
|
||||
"plane.worker": {"level": "INFO", "handlers": ["console"], "propagate": False},
|
||||
"plane.exception": {
|
||||
"level": "ERROR",
|
||||
"handlers": ["console"],
|
||||
"propagate": False,
|
||||
},
|
||||
"plane.external": {
|
||||
"level": "INFO",
|
||||
"handlers": ["console"],
|
||||
"propagate": False,
|
||||
},
|
||||
"plane.mongo": {
|
||||
"level": "INFO",
|
||||
"handlers": ["console"],
|
||||
"propagate": False,
|
||||
},
|
||||
"plane.authentication": {
|
||||
"level": "INFO",
|
||||
"handlers": ["console"],
|
||||
"propagate": False,
|
||||
},
|
||||
"plane.migrations": {
|
||||
"level": "INFO",
|
||||
"handlers": ["console"],
|
||||
"propagate": False,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
# 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.conf import settings
|
||||
import logging
|
||||
|
||||
# Third party imports
|
||||
from pymongo import MongoClient
|
||||
from pymongo.database import Database
|
||||
from pymongo.collection import Collection
|
||||
from typing import Optional, TypeVar, Type
|
||||
|
||||
|
||||
T = TypeVar("T", bound="MongoConnection")
|
||||
|
||||
# Set up logger
|
||||
logger = logging.getLogger("plane.mongo")
|
||||
|
||||
|
||||
class MongoConnection:
|
||||
"""
|
||||
A singleton class that manages MongoDB connections.
|
||||
|
||||
This class ensures only one MongoDB connection is maintained throughout the application.
|
||||
It provides methods to access the MongoDB client, database, and collections.
|
||||
|
||||
Attributes:
|
||||
_instance (Optional[MongoConnection]): The singleton instance of this class
|
||||
_client (Optional[MongoClient]): The MongoDB client instance
|
||||
_db (Optional[Database]): The MongoDB database instance
|
||||
"""
|
||||
|
||||
_instance: Optional["MongoConnection"] = None
|
||||
_client: Optional[MongoClient] = None
|
||||
_db: Optional[Database] = None
|
||||
|
||||
def __new__(cls: Type[T]) -> T:
|
||||
"""
|
||||
Creates a new instance of MongoConnection if one doesn't exist.
|
||||
|
||||
Returns:
|
||||
MongoConnection: The singleton instance
|
||||
"""
|
||||
if cls._instance is None:
|
||||
cls._instance = super(MongoConnection, cls).__new__(cls)
|
||||
try:
|
||||
mongo_url = getattr(settings, "MONGO_DB_URL", None)
|
||||
mongo_db_database = getattr(settings, "MONGO_DB_DATABASE", None)
|
||||
|
||||
if not mongo_url or not mongo_db_database:
|
||||
logger.warning(
|
||||
"MongoDB connection parameters not configured. MongoDB functionality will be disabled."
|
||||
)
|
||||
return cls._instance
|
||||
|
||||
cls._client = MongoClient(mongo_url)
|
||||
cls._db = cls._client[mongo_db_database]
|
||||
|
||||
# Test the connection
|
||||
cls._client.server_info()
|
||||
logger.info("MongoDB connection established successfully")
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Failed to initialize MongoDB connection: {str(e)}. MongoDB functionality will be disabled."
|
||||
)
|
||||
return cls._instance
|
||||
|
||||
@classmethod
|
||||
def get_client(cls) -> Optional[MongoClient]:
|
||||
"""
|
||||
Returns the MongoDB client instance.
|
||||
|
||||
Returns:
|
||||
Optional[MongoClient]: The MongoDB client instance or None if not configured
|
||||
"""
|
||||
if cls._client is None:
|
||||
cls._instance = cls()
|
||||
return cls._client
|
||||
|
||||
@classmethod
|
||||
def get_db(cls) -> Optional[Database]:
|
||||
"""
|
||||
Returns the MongoDB database instance.
|
||||
|
||||
Returns:
|
||||
Optional[Database]: The MongoDB database instance or None if not configured
|
||||
"""
|
||||
if cls._db is None:
|
||||
cls._instance = cls()
|
||||
return cls._db
|
||||
|
||||
@classmethod
|
||||
def get_collection(cls, collection_name: str) -> Optional[Collection]:
|
||||
"""
|
||||
Returns a MongoDB collection by name.
|
||||
|
||||
Args:
|
||||
collection_name (str): The name of the collection to retrieve
|
||||
|
||||
Returns:
|
||||
Optional[Collection]: The MongoDB collection instance or None if not configured
|
||||
"""
|
||||
try:
|
||||
db = cls.get_db()
|
||||
if db is None:
|
||||
logger.warning(f"Cannot access collection '{collection_name}': MongoDB not configured")
|
||||
return None
|
||||
return db[collection_name]
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to access collection '{collection_name}': {str(e)}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def is_configured(cls) -> bool:
|
||||
"""
|
||||
Check if MongoDB is properly configured and connected.
|
||||
|
||||
Returns:
|
||||
bool: True if MongoDB is configured and connected, False otherwise
|
||||
"""
|
||||
|
||||
if cls._client is None:
|
||||
cls._instance = cls()
|
||||
return cls._client is not None and cls._db is not None
|
||||
@@ -0,0 +1,276 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
"""
|
||||
OpenAPI/Swagger configuration for drf-spectacular.
|
||||
|
||||
This file contains the complete configuration for API documentation generation.
|
||||
"""
|
||||
|
||||
SPECTACULAR_SETTINGS = {
|
||||
# ========================================================================
|
||||
# Basic API Information
|
||||
# ========================================================================
|
||||
"TITLE": "The Plane REST API",
|
||||
"DESCRIPTION": (
|
||||
"The Plane REST API\n\n"
|
||||
"Visit our quick start guide and full API documentation at "
|
||||
"[developers.plane.so](https://developers.plane.so/api-reference/introduction)."
|
||||
),
|
||||
"CONTACT": {
|
||||
"name": "Plane",
|
||||
"url": "https://plane.so",
|
||||
"email": "support@plane.so",
|
||||
},
|
||||
"VERSION": "0.0.1",
|
||||
"LICENSE": {
|
||||
"name": "GNU AGPLv3",
|
||||
"url": "https://github.com/makeplane/plane/blob/preview/LICENSE.txt",
|
||||
},
|
||||
# ========================================================================
|
||||
# Schema Generation Settings
|
||||
# ========================================================================
|
||||
"SERVE_INCLUDE_SCHEMA": False,
|
||||
"SCHEMA_PATH_PREFIX": "/api/v1/",
|
||||
"SCHEMA_CACHE_TIMEOUT": 0, # disables caching
|
||||
# ========================================================================
|
||||
# Processing Hooks
|
||||
# ========================================================================
|
||||
"PREPROCESSING_HOOKS": [
|
||||
"plane.utils.openapi.hooks.preprocess_filter_api_v1_paths",
|
||||
],
|
||||
# ========================================================================
|
||||
# Server Configuration
|
||||
# ========================================================================
|
||||
"SERVERS": [
|
||||
{"url": "http://localhost:8000", "description": "Local"},
|
||||
{"url": "https://api.plane.so", "description": "Production"},
|
||||
],
|
||||
# ========================================================================
|
||||
# API Tag Definitions
|
||||
# ========================================================================
|
||||
"TAGS": [
|
||||
# System Features
|
||||
{
|
||||
"name": "Assets",
|
||||
"description": (
|
||||
"**File Upload & Presigned URLs**\n\n"
|
||||
"Generate presigned URLs for direct file uploads to cloud storage. Handle user avatars, "
|
||||
"cover images, and generic project assets with secure upload workflows.\n\n"
|
||||
"*Key Features:*\n"
|
||||
"- Generate presigned URLs for S3 uploads\n"
|
||||
"- Support for user avatars and cover images\n"
|
||||
"- Generic asset upload for projects\n"
|
||||
"- File validation and size limits\n\n"
|
||||
"*Use Cases:* User profile images, project file uploads, secure direct-to-cloud uploads."
|
||||
),
|
||||
},
|
||||
# Project Organization
|
||||
{
|
||||
"name": "Cycles",
|
||||
"description": (
|
||||
"**Sprint & Development Cycles**\n\n"
|
||||
"Create and manage development cycles (sprints) to organize work into time-boxed iterations. "
|
||||
"Track progress, assign work items, and monitor team velocity.\n\n"
|
||||
"*Key Features:*\n"
|
||||
"- Create and configure development cycles\n"
|
||||
"- Assign work items to cycles\n"
|
||||
"- Track cycle progress and completion\n"
|
||||
"- Generate cycle analytics and reports\n\n"
|
||||
"*Use Cases:* Sprint planning, iterative development, progress tracking, team velocity."
|
||||
),
|
||||
},
|
||||
# System Features
|
||||
{
|
||||
"name": "Intake",
|
||||
"description": (
|
||||
"**Work Item Intake Queue**\n\n"
|
||||
"Manage incoming work items through a dedicated intake queue for triage and review. "
|
||||
"Submit, update, and process work items before they enter the main project workflow.\n\n"
|
||||
"*Key Features:*\n"
|
||||
"- Submit work items to intake queue\n"
|
||||
"- Review and triage incoming work items\n"
|
||||
"- Update intake work item status and properties\n"
|
||||
"- Accept, reject, or modify work items before approval\n\n"
|
||||
"*Use Cases:* Work item triage, external submissions, quality review, approval workflows."
|
||||
),
|
||||
},
|
||||
# Project Organization
|
||||
{
|
||||
"name": "Labels",
|
||||
"description": (
|
||||
"**Labels & Tags**\n\n"
|
||||
"Create and manage labels to categorize and organize work items. Use color-coded labels "
|
||||
"for easy identification, filtering, and project organization.\n\n"
|
||||
"*Key Features:*\n"
|
||||
"- Create custom labels with colors and descriptions\n"
|
||||
"- Apply labels to work items for categorization\n"
|
||||
"- Filter and search by labels\n"
|
||||
"- Organize labels across projects\n\n"
|
||||
"*Use Cases:* Priority marking, feature categorization, bug classification, team organization."
|
||||
),
|
||||
},
|
||||
# Team & User Management
|
||||
{
|
||||
"name": "Members",
|
||||
"description": (
|
||||
"**Team Member Management**\n\n"
|
||||
"Manage team members, roles, and permissions within projects and workspaces. "
|
||||
"Control access levels and track member participation.\n\n"
|
||||
"*Key Features:*\n"
|
||||
"- Invite and manage team members\n"
|
||||
"- Assign roles and permissions\n"
|
||||
"- Control project and workspace access\n"
|
||||
"- Track member activity and participation\n\n"
|
||||
"*Use Cases:* Team setup, access control, role management, collaboration."
|
||||
),
|
||||
},
|
||||
# Project Organization
|
||||
{
|
||||
"name": "Modules",
|
||||
"description": (
|
||||
"**Feature Modules**\n\n"
|
||||
"Group related work items into modules for better organization and tracking. "
|
||||
"Plan features, track progress, and manage deliverables at a higher level.\n\n"
|
||||
"*Key Features:*\n"
|
||||
"- Create and organize feature modules\n"
|
||||
"- Group work items by module\n"
|
||||
"- Track module progress and completion\n"
|
||||
"- Manage module leads and assignments\n\n"
|
||||
"*Use Cases:* Feature planning, release organization, progress tracking, team coordination."
|
||||
),
|
||||
},
|
||||
# Core Project Management
|
||||
{
|
||||
"name": "Projects",
|
||||
"description": (
|
||||
"**Project Management**\n\n"
|
||||
"Create and manage projects to organize your development work. Configure project settings, "
|
||||
"manage team access, and control project visibility.\n\n"
|
||||
"*Key Features:*\n"
|
||||
"- Create, update, and delete projects\n"
|
||||
"- Configure project settings and preferences\n"
|
||||
"- Manage team access and permissions\n"
|
||||
"- Control project visibility and sharing\n\n"
|
||||
"*Use Cases:* Project setup, team collaboration, access control, project configuration."
|
||||
),
|
||||
},
|
||||
# Project Organization
|
||||
{
|
||||
"name": "States",
|
||||
"description": (
|
||||
"**Workflow States**\n\n"
|
||||
"Define custom workflow states for work items to match your team's process. "
|
||||
"Configure state transitions and track work item progress through different stages.\n\n"
|
||||
"*Key Features:*\n"
|
||||
"- Create custom workflow states\n"
|
||||
"- Configure state transitions and rules\n"
|
||||
"- Track work item progress through states\n"
|
||||
"- Set state-based permissions and automation\n\n"
|
||||
"*Use Cases:* Custom workflows, status tracking, process automation, progress monitoring."
|
||||
),
|
||||
},
|
||||
# Team & User Management
|
||||
{
|
||||
"name": "Users",
|
||||
"description": (
|
||||
"**Current User Information**\n\n"
|
||||
"Get information about the currently authenticated user including profile details "
|
||||
"and account settings.\n\n"
|
||||
"*Key Features:*\n"
|
||||
"- Retrieve current user profile\n"
|
||||
"- Access user account information\n"
|
||||
"- View user preferences and settings\n"
|
||||
"- Get authentication context\n\n"
|
||||
"*Use Cases:* Profile display, user context, account information, authentication status."
|
||||
),
|
||||
},
|
||||
# Work Item Management
|
||||
{
|
||||
"name": "Work Item Activity",
|
||||
"description": (
|
||||
"**Activity History & Search**\n\n"
|
||||
"View activity history and search for work items across the workspace. "
|
||||
"Get detailed activity logs and find work items using text search.\n\n"
|
||||
"*Key Features:*\n"
|
||||
"- View work item activity history\n"
|
||||
"- Search work items across workspace\n"
|
||||
"- Track changes and modifications\n"
|
||||
"- Filter search results by project\n\n"
|
||||
"*Use Cases:* Activity tracking, work item discovery, change history, workspace search."
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "Work Item Attachments",
|
||||
"description": (
|
||||
"**Work Item File Attachments**\n\n"
|
||||
"Generate presigned URLs for uploading files directly to specific work items. "
|
||||
"Upload and manage attachments associated with work items.\n\n"
|
||||
"*Key Features:*\n"
|
||||
"- Generate presigned URLs for work item attachments\n"
|
||||
"- Upload files directly to work items\n"
|
||||
"- Retrieve and manage attachment metadata\n"
|
||||
"- Delete attachments from work items\n\n"
|
||||
"*Use Cases:* Screenshots, error logs, design files, supporting documents."
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "Work Item Comments",
|
||||
"description": (
|
||||
"**Comments & Discussions**\n\n"
|
||||
"Add comments and discussions to work items for team collaboration. "
|
||||
"Support threaded conversations, mentions, and rich text formatting.\n\n"
|
||||
"*Key Features:*\n"
|
||||
"- Add comments to work items\n"
|
||||
"- Thread conversations and replies\n"
|
||||
"- Mention users and trigger notifications\n"
|
||||
"- Rich text and markdown support\n\n"
|
||||
"*Use Cases:* Team discussions, progress updates, code reviews, decision tracking."
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "Work Item Links",
|
||||
"description": (
|
||||
"**External Links & References**\n\n"
|
||||
"Link work items to external resources like documentation, repositories, or design files. "
|
||||
"Maintain connections between work items and external systems.\n\n"
|
||||
"*Key Features:*\n"
|
||||
"- Add external URL links to work items\n"
|
||||
"- Validate and preview linked resources\n"
|
||||
"- Organize links by type and category\n"
|
||||
"- Track link usage and access\n\n"
|
||||
"*Use Cases:* Documentation links, repository connections, design references, external tools."
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "Work Items",
|
||||
"description": (
|
||||
"**Work Items & Tasks**\n\n"
|
||||
"Create and manage work items like tasks, bugs, features, and user stories. "
|
||||
"The core entities for tracking work in your projects.\n\n"
|
||||
"*Key Features:*\n"
|
||||
"- Create, update, and manage work items\n"
|
||||
"- Assign to team members and set priorities\n"
|
||||
"- Track progress through workflow states\n"
|
||||
"- Set due dates, estimates, and relationships\n\n"
|
||||
"*Use Cases:* Bug tracking, task management, feature development, sprint planning."
|
||||
),
|
||||
},
|
||||
],
|
||||
# ========================================================================
|
||||
# Security & Authentication
|
||||
# ========================================================================
|
||||
"AUTHENTICATION_WHITELIST": [
|
||||
"plane.api.middleware.api_authentication.APIKeyAuthentication",
|
||||
],
|
||||
# ========================================================================
|
||||
# Schema Generation Options
|
||||
# ========================================================================
|
||||
"COMPONENT_NO_READ_ONLY_REQUIRED": True,
|
||||
"COMPONENT_SPLIT_REQUEST": True,
|
||||
"ENUM_NAME_OVERRIDES": {
|
||||
"ModuleStatusEnum": "plane.db.models.module.ModuleStatus",
|
||||
"IntakeWorkItemStatusEnum": "plane.db.models.intake.IntakeIssueStatus",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
"""Production settings"""
|
||||
|
||||
import os
|
||||
|
||||
from .common import * # noqa
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = int(os.environ.get("DEBUG", 0)) == 1
|
||||
|
||||
# Honor the 'X-Forwarded-Proto' header for request.is_secure()
|
||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
|
||||
INSTALLED_APPS += ("scout_apm.django",) # noqa
|
||||
|
||||
|
||||
# Scout Settings
|
||||
SCOUT_MONITOR = os.environ.get("SCOUT_MONITOR", False)
|
||||
SCOUT_KEY = os.environ.get("SCOUT_KEY", "")
|
||||
SCOUT_NAME = "Plane"
|
||||
|
||||
LOG_DIR = os.path.join(BASE_DIR, "logs") # noqa
|
||||
|
||||
if not os.path.exists(LOG_DIR):
|
||||
os.makedirs(LOG_DIR)
|
||||
|
||||
# Logging configuration
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": True,
|
||||
"formatters": {
|
||||
"verbose": {"format": "%(asctime)s [%(process)d] %(levelname)s %(name)s: %(message)s"},
|
||||
"json": {
|
||||
"()": "pythonjsonlogger.jsonlogger.JsonFormatter",
|
||||
"fmt": "%(levelname)s %(asctime)s %(module)s %(name)s %(message)s",
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "json",
|
||||
"level": "INFO",
|
||||
},
|
||||
"file": {
|
||||
"class": "plane.utils.logging.SizedTimedRotatingFileHandler",
|
||||
"filename": (
|
||||
os.path.join(BASE_DIR, "logs", "plane-debug.log") # noqa
|
||||
if DEBUG
|
||||
else os.path.join(BASE_DIR, "logs", "plane-error.log") # noqa
|
||||
),
|
||||
"when": "s",
|
||||
"maxBytes": 1024 * 1024 * 1,
|
||||
"interval": 1,
|
||||
"backupCount": 5,
|
||||
"formatter": "json",
|
||||
"level": "DEBUG" if DEBUG else "ERROR",
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"plane.api.request": {
|
||||
"level": "DEBUG" if DEBUG else "INFO",
|
||||
"handlers": ["console"],
|
||||
"propagate": False,
|
||||
},
|
||||
"plane.api": {
|
||||
"level": "DEBUG" if DEBUG else "INFO",
|
||||
"handlers": ["console"],
|
||||
"propagate": False,
|
||||
},
|
||||
"plane.worker": {
|
||||
"level": "DEBUG" if DEBUG else "INFO",
|
||||
"handlers": ["console"],
|
||||
"propagate": False,
|
||||
},
|
||||
"plane.exception": {
|
||||
"level": "DEBUG" if DEBUG else "ERROR",
|
||||
"handlers": ["console", "file"],
|
||||
"propagate": False,
|
||||
},
|
||||
"plane.external": {
|
||||
"level": "INFO",
|
||||
"handlers": ["console"],
|
||||
"propagate": False,
|
||||
},
|
||||
"plane.mongo": {
|
||||
"level": "INFO",
|
||||
"handlers": ["console"],
|
||||
"propagate": False,
|
||||
},
|
||||
"plane.authentication": {
|
||||
"level": "DEBUG" if DEBUG else "INFO",
|
||||
"handlers": ["console"],
|
||||
"propagate": False,
|
||||
},
|
||||
"plane.migrations": {
|
||||
"level": "DEBUG" if DEBUG else "INFO",
|
||||
"handlers": ["console"],
|
||||
"propagate": False,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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 redis
|
||||
from django.conf import settings
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
def redis_instance():
|
||||
# connect to redis
|
||||
if settings.REDIS_SSL:
|
||||
url = urlparse(settings.REDIS_URL)
|
||||
ri = redis.Redis(
|
||||
host=url.hostname,
|
||||
port=url.port,
|
||||
password=url.password,
|
||||
ssl=True,
|
||||
ssl_cert_reqs=None,
|
||||
)
|
||||
else:
|
||||
ri = redis.Redis.from_url(settings.REDIS_URL, db=0)
|
||||
|
||||
return ri
|
||||
@@ -0,0 +1,205 @@
|
||||
# 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 uuid
|
||||
|
||||
# Third party imports
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
from urllib.parse import quote
|
||||
|
||||
# Module imports
|
||||
from plane.utils.exception_logger import log_exception
|
||||
from storages.backends.s3boto3 import S3Boto3Storage
|
||||
|
||||
|
||||
class S3Storage(S3Boto3Storage):
|
||||
def url(self, name, parameters=None, expire=None, http_method=None):
|
||||
return name
|
||||
|
||||
"""S3 storage class to generate presigned URLs for S3 objects"""
|
||||
|
||||
def __init__(self, request=None):
|
||||
# Get the AWS credentials and bucket name from the environment
|
||||
self.aws_access_key_id = os.environ.get("AWS_ACCESS_KEY_ID")
|
||||
# Use the AWS_SECRET_ACCESS_KEY environment variable for the secret key
|
||||
self.aws_secret_access_key = os.environ.get("AWS_SECRET_ACCESS_KEY")
|
||||
# Use the AWS_S3_BUCKET_NAME environment variable for the bucket name
|
||||
self.aws_storage_bucket_name = os.environ.get("AWS_S3_BUCKET_NAME")
|
||||
# Use the AWS_REGION environment variable for the region
|
||||
self.aws_region = os.environ.get("AWS_REGION")
|
||||
# Use the AWS_S3_ENDPOINT_URL environment variable for the endpoint URL
|
||||
self.aws_s3_endpoint_url = os.environ.get("AWS_S3_ENDPOINT_URL") or os.environ.get("MINIO_ENDPOINT_URL")
|
||||
# Use the SIGNED_URL_EXPIRATION environment variable for the expiration time (default: 3600 seconds)
|
||||
self.signed_url_expiration = int(os.environ.get("SIGNED_URL_EXPIRATION", "3600"))
|
||||
|
||||
if os.environ.get("USE_MINIO") == "1":
|
||||
# Determine protocol based on environment variable
|
||||
if os.environ.get("MINIO_ENDPOINT_SSL") == "1":
|
||||
endpoint_protocol = "https"
|
||||
else:
|
||||
endpoint_protocol = request.scheme if request else "http"
|
||||
# Create an S3 client for MinIO
|
||||
self.s3_client = boto3.client(
|
||||
"s3",
|
||||
aws_access_key_id=self.aws_access_key_id,
|
||||
aws_secret_access_key=self.aws_secret_access_key,
|
||||
region_name=self.aws_region,
|
||||
endpoint_url=(f"{endpoint_protocol}://{request.get_host()}" if request else self.aws_s3_endpoint_url),
|
||||
config=boto3.session.Config(signature_version="s3v4"),
|
||||
)
|
||||
else:
|
||||
# Create an S3 client
|
||||
self.s3_client = boto3.client(
|
||||
"s3",
|
||||
aws_access_key_id=self.aws_access_key_id,
|
||||
aws_secret_access_key=self.aws_secret_access_key,
|
||||
region_name=self.aws_region,
|
||||
endpoint_url=self.aws_s3_endpoint_url,
|
||||
config=boto3.session.Config(signature_version="s3v4"),
|
||||
)
|
||||
|
||||
def generate_presigned_post(self, object_name, file_type, file_size, expiration=None):
|
||||
"""Generate a presigned URL to upload an S3 object"""
|
||||
if expiration is None:
|
||||
expiration = self.signed_url_expiration
|
||||
fields = {"Content-Type": file_type}
|
||||
|
||||
conditions = [
|
||||
{"bucket": self.aws_storage_bucket_name},
|
||||
["content-length-range", 1, file_size],
|
||||
{"Content-Type": file_type},
|
||||
]
|
||||
|
||||
# Add condition for the object name (key)
|
||||
if object_name.startswith("${filename}"):
|
||||
conditions.append(["starts-with", "$key", object_name[: -len("${filename}")]])
|
||||
else:
|
||||
fields["key"] = object_name
|
||||
conditions.append({"key": object_name})
|
||||
|
||||
# Generate the presigned POST URL
|
||||
try:
|
||||
# Generate a presigned URL for the S3 object
|
||||
response = self.s3_client.generate_presigned_post(
|
||||
Bucket=self.aws_storage_bucket_name,
|
||||
Key=object_name,
|
||||
Fields=fields,
|
||||
Conditions=conditions,
|
||||
ExpiresIn=expiration,
|
||||
)
|
||||
# Handle errors
|
||||
except ClientError as e:
|
||||
print(f"Error generating presigned POST URL: {e}")
|
||||
return None
|
||||
|
||||
return response
|
||||
|
||||
def _get_content_disposition(self, disposition, filename=None):
|
||||
"""Helper method to generate Content-Disposition header value"""
|
||||
if filename is None:
|
||||
filename = uuid.uuid4().hex
|
||||
|
||||
if filename:
|
||||
# Encode the filename to handle special characters
|
||||
encoded_filename = quote(filename)
|
||||
return f"{disposition}; filename*=UTF-8''{encoded_filename}"
|
||||
return disposition
|
||||
|
||||
def generate_presigned_url(
|
||||
self,
|
||||
object_name,
|
||||
expiration=None,
|
||||
http_method="GET",
|
||||
disposition="inline",
|
||||
filename=None,
|
||||
):
|
||||
"""Generate a presigned URL to share an S3 object"""
|
||||
if expiration is None:
|
||||
expiration = self.signed_url_expiration
|
||||
content_disposition = self._get_content_disposition(disposition, filename)
|
||||
try:
|
||||
response = self.s3_client.generate_presigned_url(
|
||||
"get_object",
|
||||
Params={
|
||||
"Bucket": self.aws_storage_bucket_name,
|
||||
"Key": str(object_name),
|
||||
"ResponseContentDisposition": content_disposition,
|
||||
},
|
||||
ExpiresIn=expiration,
|
||||
HttpMethod=http_method,
|
||||
)
|
||||
except ClientError as e:
|
||||
log_exception(e)
|
||||
return None
|
||||
|
||||
# The response contains the presigned URL
|
||||
return response
|
||||
|
||||
def get_object_metadata(self, object_name):
|
||||
"""Get the metadata for an S3 object"""
|
||||
try:
|
||||
response = self.s3_client.head_object(Bucket=self.aws_storage_bucket_name, Key=object_name)
|
||||
except ClientError as e:
|
||||
log_exception(e)
|
||||
return None
|
||||
|
||||
return {
|
||||
"ContentType": response.get("ContentType"),
|
||||
"ContentLength": response.get("ContentLength"),
|
||||
"LastModified": (response.get("LastModified").isoformat() if response.get("LastModified") else None),
|
||||
"ETag": response.get("ETag"),
|
||||
"Metadata": response.get("Metadata", {}),
|
||||
}
|
||||
|
||||
def copy_object(self, object_name, new_object_name):
|
||||
"""Copy an S3 object to a new location"""
|
||||
try:
|
||||
response = self.s3_client.copy_object(
|
||||
Bucket=self.aws_storage_bucket_name,
|
||||
CopySource={"Bucket": self.aws_storage_bucket_name, "Key": object_name},
|
||||
Key=new_object_name,
|
||||
)
|
||||
except ClientError as e:
|
||||
log_exception(e)
|
||||
return None
|
||||
|
||||
return response
|
||||
|
||||
def upload_file(
|
||||
self,
|
||||
file_obj,
|
||||
object_name: str,
|
||||
content_type: str = None,
|
||||
extra_args: dict = {},
|
||||
) -> bool:
|
||||
"""Upload a file directly to S3"""
|
||||
try:
|
||||
if content_type:
|
||||
extra_args["ContentType"] = content_type
|
||||
|
||||
self.s3_client.upload_fileobj(
|
||||
file_obj,
|
||||
self.aws_storage_bucket_name,
|
||||
object_name,
|
||||
ExtraArgs=extra_args,
|
||||
)
|
||||
return True
|
||||
except ClientError as e:
|
||||
log_exception(e)
|
||||
return False
|
||||
|
||||
def delete_files(self, object_names):
|
||||
"""Delete an S3 object"""
|
||||
try:
|
||||
self.s3_client.delete_objects(
|
||||
Bucket=self.aws_storage_bucket_name,
|
||||
Delete={"Objects": [{"Key": object_name} for object_name in object_names]},
|
||||
)
|
||||
return True
|
||||
except ClientError as e:
|
||||
log_exception(e)
|
||||
return False
|
||||
@@ -0,0 +1,16 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
"""Test Settings"""
|
||||
|
||||
from .common import * # noqa
|
||||
|
||||
DEBUG = True
|
||||
|
||||
# Send it in a dummy outbox
|
||||
EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
|
||||
|
||||
INSTALLED_APPS.append( # noqa
|
||||
"plane.tests"
|
||||
)
|
||||
Reference in New Issue
Block a user