mirror of
https://github.com/makeplane/plane
synced 2025-08-07 19:59:33 +00:00
Compare commits
5 Commits
develop
...
feat-works
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c1b100ee0 | ||
|
|
8e725b0f75 | ||
|
|
cf75cb35b1 | ||
|
|
0487579f4e | ||
|
|
60232130f4 |
@@ -1,33 +1,31 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
|
|
||||||
from plane.app.views import (
|
from plane.app.views import (
|
||||||
UserWorkspaceInvitationsViewSet,
|
ExportWorkspaceUserActivityEndpoint,
|
||||||
WorkSpaceViewSet,
|
|
||||||
WorkspaceJoinEndpoint,
|
|
||||||
WorkSpaceMemberViewSet,
|
|
||||||
WorkspaceInvitationsViewset,
|
|
||||||
WorkspaceMemberUserEndpoint,
|
|
||||||
WorkspaceMemberUserViewsEndpoint,
|
|
||||||
WorkSpaceAvailabilityCheckEndpoint,
|
|
||||||
TeamMemberViewSet,
|
TeamMemberViewSet,
|
||||||
UserLastProjectWithWorkspaceEndpoint,
|
UserLastProjectWithWorkspaceEndpoint,
|
||||||
|
UserWorkspaceInvitationsViewSet,
|
||||||
|
WorkSpaceAvailabilityCheckEndpoint,
|
||||||
|
WorkspaceCyclesEndpoint,
|
||||||
|
WorkspaceEstimatesEndpoint,
|
||||||
|
WorkspaceInvitationsViewset,
|
||||||
|
WorkspaceJoinEndpoint,
|
||||||
|
WorkspaceLabelsEndpoint,
|
||||||
|
WorkspaceMemberUserEndpoint,
|
||||||
|
WorkspaceMemberUserViewsEndpoint,
|
||||||
|
WorkSpaceMemberViewSet,
|
||||||
|
WorkspaceModulesEndpoint,
|
||||||
|
WorkspaceProjectMemberEndpoint,
|
||||||
|
WorkspaceStatesEndpoint,
|
||||||
WorkspaceThemeViewSet,
|
WorkspaceThemeViewSet,
|
||||||
WorkspaceUserProfileStatsEndpoint,
|
|
||||||
WorkspaceUserActivityEndpoint,
|
WorkspaceUserActivityEndpoint,
|
||||||
WorkspaceUserProfileEndpoint,
|
WorkspaceUserProfileEndpoint,
|
||||||
WorkspaceUserProfileIssuesEndpoint,
|
WorkspaceUserProfileIssuesEndpoint,
|
||||||
WorkspaceLabelsEndpoint,
|
WorkspaceUserProfileStatsEndpoint,
|
||||||
WorkspaceProjectMemberEndpoint,
|
|
||||||
WorkspaceUserPropertiesEndpoint,
|
WorkspaceUserPropertiesEndpoint,
|
||||||
WorkspaceStatesEndpoint,
|
WorkSpaceViewSet,
|
||||||
WorkspaceEstimatesEndpoint,
|
|
||||||
ExportWorkspaceUserActivityEndpoint,
|
|
||||||
WorkspaceModulesEndpoint,
|
|
||||||
WorkspaceCyclesEndpoint,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path(
|
path(
|
||||||
"workspace-slug-check/",
|
"workspace-slug-check/",
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ from .workspace.base import (
|
|||||||
WorkSpaceAvailabilityCheckEndpoint,
|
WorkSpaceAvailabilityCheckEndpoint,
|
||||||
UserWorkspaceDashboardEndpoint,
|
UserWorkspaceDashboardEndpoint,
|
||||||
WorkspaceThemeViewSet,
|
WorkspaceThemeViewSet,
|
||||||
ExportWorkspaceUserActivityEndpoint
|
ExportWorkspaceUserActivityEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .workspace.member import (
|
from .workspace.member import (
|
||||||
|
|||||||
311
apiserver/plane/bgtasks/workspace_export_task.py
Normal file
311
apiserver/plane/bgtasks/workspace_export_task.py
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
# Python imports
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
import boto3
|
||||||
|
from botocore.client import Config
|
||||||
|
from celery import shared_task
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.mail import EmailMultiAlternatives, get_connection
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
from django.template.loader import render_to_string
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.html import strip_tags
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.db.models import (
|
||||||
|
APIToken,
|
||||||
|
CommentReaction,
|
||||||
|
Cycle,
|
||||||
|
CycleFavorite,
|
||||||
|
CycleIssue,
|
||||||
|
CycleUserProperties,
|
||||||
|
Estimate,
|
||||||
|
EstimatePoint,
|
||||||
|
FileAsset,
|
||||||
|
Inbox,
|
||||||
|
InboxIssue,
|
||||||
|
Issue,
|
||||||
|
IssueActivity,
|
||||||
|
IssueAssignee,
|
||||||
|
IssueAttachment,
|
||||||
|
IssueComment,
|
||||||
|
IssueLabel,
|
||||||
|
IssueLink,
|
||||||
|
IssueMention,
|
||||||
|
IssueProperty,
|
||||||
|
IssueReaction,
|
||||||
|
IssueRelation,
|
||||||
|
IssueSequence,
|
||||||
|
IssueSubscriber,
|
||||||
|
IssueView,
|
||||||
|
IssueViewFavorite,
|
||||||
|
IssueVote,
|
||||||
|
Label,
|
||||||
|
Module,
|
||||||
|
ModuleFavorite,
|
||||||
|
ModuleIssue,
|
||||||
|
ModuleLink,
|
||||||
|
ModuleMember,
|
||||||
|
ModuleUserProperties,
|
||||||
|
Notification,
|
||||||
|
Page,
|
||||||
|
PageFavorite,
|
||||||
|
PageLabel,
|
||||||
|
PageLog,
|
||||||
|
Project,
|
||||||
|
ProjectDeployBoard,
|
||||||
|
ProjectFavorite,
|
||||||
|
ProjectIdentifier,
|
||||||
|
ProjectMember,
|
||||||
|
ProjectMemberInvite,
|
||||||
|
ProjectPublicMember,
|
||||||
|
State,
|
||||||
|
User,
|
||||||
|
UserNotificationPreference,
|
||||||
|
Webhook,
|
||||||
|
Workspace,
|
||||||
|
WorkspaceMember,
|
||||||
|
WorkspaceMemberInvite,
|
||||||
|
WorkspaceTheme,
|
||||||
|
WorkspaceUserProperties,
|
||||||
|
)
|
||||||
|
from plane.license.utils.instance_value import get_email_configuration
|
||||||
|
from plane.utils.exception_logger import log_exception
|
||||||
|
|
||||||
|
|
||||||
|
def create_zip_file(files):
|
||||||
|
# Create zip
|
||||||
|
zip_buffer = io.BytesIO()
|
||||||
|
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zipf:
|
||||||
|
for file in files:
|
||||||
|
filename = file.get("filename")
|
||||||
|
file_content = file.get("data")
|
||||||
|
zipf.writestr(filename, file_content)
|
||||||
|
|
||||||
|
zip_buffer.seek(0)
|
||||||
|
return zip_buffer
|
||||||
|
|
||||||
|
|
||||||
|
def upload_to_s3(zip_file, workspace_id, slug):
|
||||||
|
# Upload the zip to s3
|
||||||
|
file_name = f"{workspace_id}/export-{slug}-{timezone.now()}.zip"
|
||||||
|
expires_in = 7 * 24 * 60 * 60
|
||||||
|
|
||||||
|
if settings.USE_MINIO:
|
||||||
|
s3 = boto3.client(
|
||||||
|
"s3",
|
||||||
|
endpoint_url=settings.AWS_S3_ENDPOINT_URL,
|
||||||
|
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||||
|
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||||
|
config=Config(signature_version="s3v4"),
|
||||||
|
)
|
||||||
|
s3.upload_fileobj(
|
||||||
|
zip_file,
|
||||||
|
settings.AWS_STORAGE_BUCKET_NAME,
|
||||||
|
file_name,
|
||||||
|
ExtraArgs={"ACL": "public-read", "ContentType": "application/zip"},
|
||||||
|
)
|
||||||
|
presigned_url = s3.generate_presigned_url(
|
||||||
|
"get_object",
|
||||||
|
Params={
|
||||||
|
"Bucket": settings.AWS_STORAGE_BUCKET_NAME,
|
||||||
|
"Key": file_name,
|
||||||
|
},
|
||||||
|
ExpiresIn=expires_in,
|
||||||
|
)
|
||||||
|
# Create the new url with updated domain and protocol
|
||||||
|
presigned_url = presigned_url.replace(
|
||||||
|
f"{settings.AWS_S3_ENDPOINT_URL}/{settings.AWS_STORAGE_BUCKET_NAME}/",
|
||||||
|
f"{settings.AWS_S3_URL_PROTOCOL}//{settings.AWS_S3_CUSTOM_DOMAIN}/",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
s3 = boto3.client(
|
||||||
|
"s3",
|
||||||
|
region_name=settings.AWS_REGION,
|
||||||
|
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||||
|
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||||
|
config=Config(signature_version="s3v4"),
|
||||||
|
)
|
||||||
|
s3.upload_fileobj(
|
||||||
|
zip_file,
|
||||||
|
settings.AWS_STORAGE_BUCKET_NAME,
|
||||||
|
file_name,
|
||||||
|
ExtraArgs={"ACL": "public-read", "ContentType": "application/zip"},
|
||||||
|
)
|
||||||
|
|
||||||
|
presigned_url = s3.generate_presigned_url(
|
||||||
|
"get_object",
|
||||||
|
Params={
|
||||||
|
"Bucket": settings.AWS_STORAGE_BUCKET_NAME,
|
||||||
|
"Key": file_name,
|
||||||
|
},
|
||||||
|
ExpiresIn=expires_in,
|
||||||
|
)
|
||||||
|
|
||||||
|
return presigned_url
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def workspace_export(workspace_id, email):
|
||||||
|
# Get the workspace
|
||||||
|
workspace = Workspace.objects.get(pk=workspace_id)
|
||||||
|
slug = workspace.slug
|
||||||
|
# Store all files
|
||||||
|
files = []
|
||||||
|
|
||||||
|
# Users that need to be exported
|
||||||
|
emails = WorkspaceMember.objects.filter(
|
||||||
|
workspace_id=workspace_id
|
||||||
|
).values_list("member__email", flat=True)
|
||||||
|
users = User.objects.filter(email__in=emails).values()
|
||||||
|
|
||||||
|
users_json = json.dumps(list(users), cls=DjangoJSONEncoder)
|
||||||
|
files.append({"filename": "users.json", "data": users_json})
|
||||||
|
|
||||||
|
workspace = list(Workspace.objects.filter(pk=workspace_id).values())
|
||||||
|
workspace_json = json.dumps(workspace, cls=DjangoJSONEncoder)
|
||||||
|
files.append({"filename": "workspaces.json", "data": workspace_json})
|
||||||
|
|
||||||
|
models = {
|
||||||
|
# Workspace
|
||||||
|
WorkspaceMemberInvite: "workspace_member_invites.json",
|
||||||
|
WorkspaceMember: "workspace_members.json",
|
||||||
|
WorkspaceTheme: "workspace_themes.json",
|
||||||
|
WorkspaceUserProperties: "workspace_user_properties.json",
|
||||||
|
# Projects
|
||||||
|
Project: "projects.json",
|
||||||
|
ProjectDeployBoard: "project_deploy_boards.json",
|
||||||
|
ProjectFavorite: "project_favorites.json",
|
||||||
|
ProjectIdentifier: "project_identifier.json",
|
||||||
|
ProjectMember: "project_members.json",
|
||||||
|
ProjectMemberInvite: "project_member_invites.json",
|
||||||
|
ProjectPublicMember: "project_public_members.json",
|
||||||
|
# APIToken
|
||||||
|
APIToken: "api_tokens.json",
|
||||||
|
# Assets
|
||||||
|
FileAsset: "file_assets.json",
|
||||||
|
# States
|
||||||
|
State: "states.json",
|
||||||
|
# Issues
|
||||||
|
Issue: "issues.json",
|
||||||
|
IssueAssignee: "issue_assignees.json",
|
||||||
|
Label: "labels.json",
|
||||||
|
IssueLabel: "issue_labels.json",
|
||||||
|
IssueLink: "issue_links.json",
|
||||||
|
IssueMention: "issue_mention.json",
|
||||||
|
IssueVote: "issue_votes.json",
|
||||||
|
IssueSubscriber: "issue_subscribers.json",
|
||||||
|
IssueProperty: "issue_properties.json",
|
||||||
|
IssueSequence: "issue_sequences.json",
|
||||||
|
IssueReaction: "issue_reactions.json",
|
||||||
|
IssueRelation: "issue_relations.json",
|
||||||
|
IssueAttachment: "issue_attachments.json",
|
||||||
|
IssueActivity: "issue_activities.json",
|
||||||
|
CommentReaction: "comment_reactions.json",
|
||||||
|
IssueComment: "issue_comments.json",
|
||||||
|
# Cycles
|
||||||
|
Cycle: "cycles.json",
|
||||||
|
CycleIssue: "cycle_issues.json",
|
||||||
|
CycleFavorite: "cycle_favorites.json",
|
||||||
|
CycleUserProperties: "cycle_user_properties.json",
|
||||||
|
# Modules
|
||||||
|
Module: "modules.json",
|
||||||
|
ModuleIssue: "module_issues.json",
|
||||||
|
ModuleFavorite: "module_favorites.json",
|
||||||
|
ModuleLink: "module_links.json",
|
||||||
|
ModuleMember: "module_members.json",
|
||||||
|
ModuleUserProperties: "module_user_properties.json",
|
||||||
|
# Page
|
||||||
|
Page: "pages.json",
|
||||||
|
PageLog: "page_logs.json",
|
||||||
|
PageLabel: "page_labels.json",
|
||||||
|
PageFavorite: "page_favorites.json",
|
||||||
|
# Estimate
|
||||||
|
Estimate: "estimates.json",
|
||||||
|
EstimatePoint: "estimate_points.json",
|
||||||
|
# Webhook
|
||||||
|
Webhook: "webhooks.json",
|
||||||
|
# Views
|
||||||
|
IssueView: "views.json",
|
||||||
|
IssueViewFavorite: "view_favorites.json",
|
||||||
|
# Notification
|
||||||
|
Notification: "notifications.json",
|
||||||
|
UserNotificationPreference: "user_notification_preferences.json",
|
||||||
|
# Inbox
|
||||||
|
Inbox: "inboxes.json",
|
||||||
|
InboxIssue: "inbox_issues.json",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Loop through the models
|
||||||
|
for model in models:
|
||||||
|
file_name = models[model]
|
||||||
|
files.append(
|
||||||
|
{
|
||||||
|
"filename": file_name,
|
||||||
|
"data": json.dumps(
|
||||||
|
list(
|
||||||
|
model.objects.filter(
|
||||||
|
workspace_id=workspace_id
|
||||||
|
).values()
|
||||||
|
),
|
||||||
|
cls=DjangoJSONEncoder,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create zip
|
||||||
|
zip_buffer = create_zip_file(files)
|
||||||
|
|
||||||
|
# Get the presigned url
|
||||||
|
url = upload_to_s3(
|
||||||
|
workspace_id=workspace_id, slug=slug, zip_file=zip_buffer
|
||||||
|
)
|
||||||
|
|
||||||
|
# Send mail
|
||||||
|
try:
|
||||||
|
(
|
||||||
|
EMAIL_HOST,
|
||||||
|
EMAIL_HOST_USER,
|
||||||
|
EMAIL_HOST_PASSWORD,
|
||||||
|
EMAIL_PORT,
|
||||||
|
EMAIL_USE_TLS,
|
||||||
|
EMAIL_FROM,
|
||||||
|
) = get_email_configuration()
|
||||||
|
|
||||||
|
# Send the mail
|
||||||
|
subject = "Your Plane Export Link"
|
||||||
|
context = {"url": url, "email": email}
|
||||||
|
|
||||||
|
html_content = render_to_string(
|
||||||
|
"emails/exports/workspace_exports.html", context
|
||||||
|
)
|
||||||
|
text_content = strip_tags(html_content)
|
||||||
|
|
||||||
|
connection = get_connection(
|
||||||
|
host=EMAIL_HOST,
|
||||||
|
port=int(EMAIL_PORT),
|
||||||
|
username=EMAIL_HOST_USER,
|
||||||
|
password=EMAIL_HOST_PASSWORD,
|
||||||
|
use_tls=EMAIL_USE_TLS == "1",
|
||||||
|
)
|
||||||
|
|
||||||
|
msg = EmailMultiAlternatives(
|
||||||
|
subject=subject,
|
||||||
|
body=text_content,
|
||||||
|
from_email=EMAIL_FROM,
|
||||||
|
to=[email],
|
||||||
|
connection=connection,
|
||||||
|
)
|
||||||
|
msg.attach_alternative(html_content, "text/html")
|
||||||
|
msg.send()
|
||||||
|
logging.getLogger("plane").info("Email sent successfully.")
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
log_exception(e)
|
||||||
|
return
|
||||||
475
apiserver/plane/bgtasks/workspace_import_task.py
Normal file
475
apiserver/plane/bgtasks/workspace_import_task.py
Normal file
@@ -0,0 +1,475 @@
|
|||||||
|
# Python imports
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
from celery import shared_task
|
||||||
|
|
||||||
|
from plane.db.models import (
|
||||||
|
APIToken,
|
||||||
|
CommentReaction,
|
||||||
|
Cycle,
|
||||||
|
CycleFavorite,
|
||||||
|
CycleIssue,
|
||||||
|
CycleUserProperties,
|
||||||
|
Estimate,
|
||||||
|
EstimatePoint,
|
||||||
|
FileAsset,
|
||||||
|
Inbox,
|
||||||
|
InboxIssue,
|
||||||
|
Issue,
|
||||||
|
IssueActivity,
|
||||||
|
IssueAssignee,
|
||||||
|
IssueAttachment,
|
||||||
|
IssueComment,
|
||||||
|
IssueLabel,
|
||||||
|
IssueLink,
|
||||||
|
IssueMention,
|
||||||
|
IssueProperty,
|
||||||
|
IssueReaction,
|
||||||
|
IssueRelation,
|
||||||
|
IssueSequence,
|
||||||
|
IssueSubscriber,
|
||||||
|
IssueView,
|
||||||
|
IssueViewFavorite,
|
||||||
|
IssueVote,
|
||||||
|
Label,
|
||||||
|
Module,
|
||||||
|
ModuleFavorite,
|
||||||
|
ModuleIssue,
|
||||||
|
ModuleLink,
|
||||||
|
ModuleMember,
|
||||||
|
ModuleUserProperties,
|
||||||
|
Notification,
|
||||||
|
Page,
|
||||||
|
PageFavorite,
|
||||||
|
PageLabel,
|
||||||
|
PageLog,
|
||||||
|
Project,
|
||||||
|
ProjectDeployBoard,
|
||||||
|
ProjectFavorite,
|
||||||
|
ProjectIdentifier,
|
||||||
|
ProjectMember,
|
||||||
|
ProjectMemberInvite,
|
||||||
|
ProjectPublicMember,
|
||||||
|
State,
|
||||||
|
User,
|
||||||
|
UserNotificationPreference,
|
||||||
|
Webhook,
|
||||||
|
Workspace,
|
||||||
|
WorkspaceMember,
|
||||||
|
WorkspaceMemberInvite,
|
||||||
|
WorkspaceTheme,
|
||||||
|
WorkspaceUserProperties,
|
||||||
|
)
|
||||||
|
from plane.utils.exception_logger import log_exception
|
||||||
|
|
||||||
|
|
||||||
|
def replace_model_data(fields, model_data, user_maps):
|
||||||
|
for field in fields:
|
||||||
|
model_data[field] = user_maps.get(
|
||||||
|
model_data.get(field), model_data.get(field)
|
||||||
|
)
|
||||||
|
return model_data
|
||||||
|
|
||||||
|
|
||||||
|
def data_transformer(model, model_data, user_maps):
|
||||||
|
# model name mapper
|
||||||
|
mapper = {
|
||||||
|
"workspace": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
"owner_id",
|
||||||
|
],
|
||||||
|
"workspacememberinvite": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
"workspacetheme": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
"workspacemember": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
"member_id",
|
||||||
|
],
|
||||||
|
"workspaceuserproperties": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
"user_id",
|
||||||
|
],
|
||||||
|
"project": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
"default_assignee_id",
|
||||||
|
"project_lead_id",
|
||||||
|
],
|
||||||
|
"projectdeployboard": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
"projectfavorite": [
|
||||||
|
"user_id",
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
"projectmember": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
"member_id",
|
||||||
|
],
|
||||||
|
"projectidentifier": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
"projectmemberinvite": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
"projectpublicmember": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
"member_id",
|
||||||
|
],
|
||||||
|
"state": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
"label": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
"estimate": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
"estimatepoint": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
"issue": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
"issuecomment": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
"actor_id",
|
||||||
|
],
|
||||||
|
"issueassignee": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
"assignee_id",
|
||||||
|
],
|
||||||
|
"issuelabel": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
"issuelink": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
"issuemention": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
"mention_id",
|
||||||
|
],
|
||||||
|
"issuevote": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
"actor_id",
|
||||||
|
],
|
||||||
|
"issuesubscriber": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
"subscriber_id",
|
||||||
|
],
|
||||||
|
"issueproperty": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
"user_id",
|
||||||
|
],
|
||||||
|
"issuesequence": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
"issuereaction": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
"actor_id",
|
||||||
|
],
|
||||||
|
"issuerelation": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
"issueattachment": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
"issueactivity": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
"actor_id",
|
||||||
|
],
|
||||||
|
"apitoken": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
"user_id",
|
||||||
|
],
|
||||||
|
"fileasset": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
"commentreaction": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
"actor_id",
|
||||||
|
],
|
||||||
|
"cycle": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
"owned_by_id",
|
||||||
|
],
|
||||||
|
"cycleissue": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
"cyclefavorite": [
|
||||||
|
"user_id",
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
"cycleuserproperties": [
|
||||||
|
"user_id",
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
"module": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
"lead_id",
|
||||||
|
],
|
||||||
|
"moduleissue": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
"modulefavorite": [
|
||||||
|
"user_id",
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
"modulelink": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
"modulemember": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
"member_id",
|
||||||
|
],
|
||||||
|
"moduleuserproperties": [
|
||||||
|
"user_id",
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
"page": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
"owned_by_id",
|
||||||
|
],
|
||||||
|
"pagelog": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
"pagelabel": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
"pagefavorite": [
|
||||||
|
"user_id",
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
"webhook": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
"issueview": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
"issueviewfavorite": [
|
||||||
|
"user_id",
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
"notification": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
"triggered_by_id",
|
||||||
|
"receiver_id",
|
||||||
|
],
|
||||||
|
"usernotificationpreference": [
|
||||||
|
"user_id",
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
"inbox": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
"inboxissue": [
|
||||||
|
"created_by_id",
|
||||||
|
"updated_by_id",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get all the fields for the current model
|
||||||
|
fields = mapper[model._meta.model_name]
|
||||||
|
if fields:
|
||||||
|
model_data = replace_model_data(
|
||||||
|
fields=fields, model_data=model_data, user_maps=user_maps
|
||||||
|
)
|
||||||
|
# return modified model data
|
||||||
|
return model_data
|
||||||
|
# return model data
|
||||||
|
return model_data
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def workspace_import(workspace_data):
|
||||||
|
try:
|
||||||
|
# Create Users
|
||||||
|
users = json.loads(workspace_data.get("users.json"))
|
||||||
|
|
||||||
|
# get user emails
|
||||||
|
user_emails = [user.get("email") for user in users]
|
||||||
|
imported_users = {user.get("email"): user.get("id") for user in users}
|
||||||
|
existing_users = User.objects.filter(email__in=user_emails).values(
|
||||||
|
"id", "email"
|
||||||
|
)
|
||||||
|
|
||||||
|
user_maps = {
|
||||||
|
imported_users.get(exuser.get("email")): str(exuser.get("id"))
|
||||||
|
for exuser in existing_users
|
||||||
|
if imported_users.get(exuser.get("email"))
|
||||||
|
}
|
||||||
|
|
||||||
|
User.objects.bulk_create(
|
||||||
|
[User(**user) for user in (users)],
|
||||||
|
batch_size=100,
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Workspaces
|
||||||
|
workspaces = workspace_data.get("workspaces.json")
|
||||||
|
Workspace.objects.bulk_create(
|
||||||
|
[
|
||||||
|
Workspace(
|
||||||
|
**data_transformer(
|
||||||
|
model=Workspace,
|
||||||
|
model_data=workspace,
|
||||||
|
user_maps=user_maps,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for workspace in json.loads(workspaces)
|
||||||
|
],
|
||||||
|
batch_size=100,
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
models = {
|
||||||
|
# Workspace
|
||||||
|
WorkspaceMemberInvite: "workspace_member_invites.json",
|
||||||
|
WorkspaceMember: "workspace_members.json",
|
||||||
|
WorkspaceTheme: "workspace_themes.json",
|
||||||
|
WorkspaceUserProperties: "workspace_user_properties.json",
|
||||||
|
# Projects
|
||||||
|
Project: "projects.json",
|
||||||
|
ProjectDeployBoard: "project_deploy_boards.json",
|
||||||
|
ProjectFavorite: "project_favorites.json",
|
||||||
|
ProjectIdentifier: "project_identifier.json",
|
||||||
|
ProjectMember: "project_members.json",
|
||||||
|
ProjectMemberInvite: "project_member_invites.json",
|
||||||
|
ProjectPublicMember: "project_public_members.json",
|
||||||
|
# States
|
||||||
|
State: "states.json",
|
||||||
|
# Labels
|
||||||
|
Label: "labels.json",
|
||||||
|
# Estimate
|
||||||
|
Estimate: "estimates.json",
|
||||||
|
EstimatePoint: "estimate_points.json",
|
||||||
|
# Issues
|
||||||
|
Issue: "issues.json",
|
||||||
|
IssueComment: "issue_comments.json",
|
||||||
|
IssueAssignee: "issue_assignees.json",
|
||||||
|
IssueLabel: "issue_labels.json",
|
||||||
|
IssueLink: "issue_links.json",
|
||||||
|
IssueMention: "issue_mention.json",
|
||||||
|
IssueVote: "issue_votes.json",
|
||||||
|
IssueSubscriber: "issue_subscribers.json",
|
||||||
|
IssueProperty: "issue_properties.json",
|
||||||
|
IssueSequence: "issue_sequences.json",
|
||||||
|
IssueReaction: "issue_reactions.json",
|
||||||
|
IssueRelation: "issue_relations.json",
|
||||||
|
IssueAttachment: "issue_attachments.json",
|
||||||
|
IssueActivity: "issue_activities.json",
|
||||||
|
# APIToken
|
||||||
|
APIToken: "api_tokens.json",
|
||||||
|
# Assets
|
||||||
|
FileAsset: "file_assets.json",
|
||||||
|
CommentReaction: "comment_reactions.json",
|
||||||
|
# Cycles
|
||||||
|
Cycle: "cycles.json",
|
||||||
|
CycleIssue: "cycle_issues.json",
|
||||||
|
CycleFavorite: "cycle_favorites.json",
|
||||||
|
CycleUserProperties: "cycle_user_properties.json",
|
||||||
|
# Modules
|
||||||
|
Module: "modules.json",
|
||||||
|
ModuleIssue: "module_issues.json",
|
||||||
|
ModuleFavorite: "module_favorites.json",
|
||||||
|
ModuleLink: "module_links.json",
|
||||||
|
ModuleMember: "module_members.json",
|
||||||
|
ModuleUserProperties: "module_user_properties",
|
||||||
|
# Page
|
||||||
|
Page: "pages.json",
|
||||||
|
PageLog: "page_logs.json",
|
||||||
|
PageLabel: "page_labels.json",
|
||||||
|
PageFavorite: "page_favorites.json",
|
||||||
|
# Webhook
|
||||||
|
Webhook: "webhooks.json",
|
||||||
|
# Views
|
||||||
|
IssueView: "views.json",
|
||||||
|
IssueViewFavorite: "view_favorites.json",
|
||||||
|
# Notification
|
||||||
|
Notification: "notifications.json",
|
||||||
|
UserNotificationPreference: "user_notification_preferences.json",
|
||||||
|
# Inbox
|
||||||
|
Inbox: "inboxes.json",
|
||||||
|
InboxIssue: "inbox_issues.json",
|
||||||
|
}
|
||||||
|
|
||||||
|
for model in models:
|
||||||
|
file_name = models[model]
|
||||||
|
data = workspace_data.get(file_name)
|
||||||
|
# Loop through all the models and create the records accordingly
|
||||||
|
model.objects.bulk_create(
|
||||||
|
[
|
||||||
|
model(
|
||||||
|
**data_transformer(
|
||||||
|
model=model,
|
||||||
|
model_data=model_data,
|
||||||
|
user_maps=user_maps,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for model_data in json.loads(data)
|
||||||
|
],
|
||||||
|
batch_size=100,
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log_exception(e)
|
||||||
|
return
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Django imports
|
# Django imports
|
||||||
from django.db import models
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from . import ProjectBaseModel
|
from . import ProjectBaseModel
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
# Django imports
|
# Django imports
|
||||||
from django.db import models
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from . import BaseModel
|
from . import BaseModel
|
||||||
|
|
||||||
|
|
||||||
ROLE_CHOICES = (
|
ROLE_CHOICES = (
|
||||||
(20, "Owner"),
|
(20, "Owner"),
|
||||||
(15, "Admin"),
|
(15, "Admin"),
|
||||||
|
|||||||
@@ -4,4 +4,6 @@ from .instance import (
|
|||||||
InstanceConfigurationEndpoint,
|
InstanceConfigurationEndpoint,
|
||||||
InstanceAdminSignInEndpoint,
|
InstanceAdminSignInEndpoint,
|
||||||
SignUpScreenVisitedEndpoint,
|
SignUpScreenVisitedEndpoint,
|
||||||
|
ExportWorkspaceEndpoint,
|
||||||
|
ImportWorkspaceEndpoint,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,33 +1,39 @@
|
|||||||
# Python imports
|
# Python imports
|
||||||
import uuid
|
import uuid
|
||||||
|
import zipfile
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.utils import timezone
|
|
||||||
from django.contrib.auth.hashers import make_password
|
from django.contrib.auth.hashers import make_password
|
||||||
from django.core.validators import validate_email
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.core.validators import validate_email
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny
|
||||||
|
from rest_framework.response import Response
|
||||||
from rest_framework_simplejwt.tokens import RefreshToken
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
|
from plane.app.serializers.workspace import WorkspaceLiteSerializer
|
||||||
from plane.app.views import BaseAPIView
|
from plane.app.views import BaseAPIView
|
||||||
from plane.license.models import Instance, InstanceAdmin, InstanceConfiguration
|
from plane.bgtasks.workspace_export_task import workspace_export
|
||||||
from plane.license.api.serializers import (
|
from plane.bgtasks.workspace_import_task import workspace_import
|
||||||
InstanceSerializer,
|
from plane.db.models import User, Workspace
|
||||||
InstanceAdminSerializer,
|
|
||||||
InstanceConfigurationSerializer,
|
|
||||||
)
|
|
||||||
from plane.license.api.permissions import (
|
from plane.license.api.permissions import (
|
||||||
InstanceAdminPermission,
|
InstanceAdminPermission,
|
||||||
)
|
)
|
||||||
from plane.db.models import User
|
from plane.license.api.serializers import (
|
||||||
|
InstanceAdminSerializer,
|
||||||
|
InstanceConfigurationSerializer,
|
||||||
|
InstanceSerializer,
|
||||||
|
)
|
||||||
|
from plane.license.models import Instance, InstanceAdmin, InstanceConfiguration
|
||||||
from plane.license.utils.encryption import encrypt_data
|
from plane.license.utils.encryption import encrypt_data
|
||||||
from plane.utils.cache import cache_response, invalidate_cache
|
from plane.utils.cache import cache_response, invalidate_cache
|
||||||
|
|
||||||
|
|
||||||
class InstanceEndpoint(BaseAPIView):
|
class InstanceEndpoint(BaseAPIView):
|
||||||
def get_permissions(self):
|
def get_permissions(self):
|
||||||
if self.request.method == "PATCH":
|
if self.request.method == "PATCH":
|
||||||
@@ -272,3 +278,84 @@ class SignUpScreenVisitedEndpoint(BaseAPIView):
|
|||||||
instance.is_signup_screen_visited = True
|
instance.is_signup_screen_visited = True
|
||||||
instance.save()
|
instance.save()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
class InstanceWorkspacesEndpoint(BaseAPIView):
|
||||||
|
|
||||||
|
permission_classes = [
|
||||||
|
InstanceAdminPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
workspaces = Workspace.objects.all()
|
||||||
|
serializer = WorkspaceLiteSerializer(workspaces, many=True)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class ExportWorkspaceEndpoint(BaseAPIView):
|
||||||
|
|
||||||
|
permission_classes = [
|
||||||
|
InstanceAdminPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
workspace_id = request.data.get("workspace_id", False)
|
||||||
|
|
||||||
|
if not workspace_id:
|
||||||
|
return Response(
|
||||||
|
{"error": "Workspace ID is required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
workspace_export.delay(
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
email=request.user.email,
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"message": "An email will be sent to download the exports when they are ready"
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ImportWorkspaceEndpoint(BaseAPIView):
|
||||||
|
|
||||||
|
parser_classes = (
|
||||||
|
MultiPartParser,
|
||||||
|
FormParser,
|
||||||
|
JSONParser,
|
||||||
|
)
|
||||||
|
|
||||||
|
permission_classes = [
|
||||||
|
InstanceAdminPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
file_obj = request.FILES.get("zip_file")
|
||||||
|
if file_obj is None:
|
||||||
|
return Response(
|
||||||
|
"No file uploaded.", status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure the uploaded file is a ZIP file
|
||||||
|
if not zipfile.is_zipfile(file_obj):
|
||||||
|
return Response(
|
||||||
|
"Uploaded file is not a valid zip file.",
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reading contents of the ZIP file
|
||||||
|
file_contents = {}
|
||||||
|
with zipfile.ZipFile(file_obj, "r") as zip_ref:
|
||||||
|
for file_name in zip_ref.namelist():
|
||||||
|
with zip_ref.open(file_name) as file:
|
||||||
|
# Assuming the file content is text. Use file.read() for binary content.
|
||||||
|
content = file.read().decode("utf-8")
|
||||||
|
file_contents[file_name] = content
|
||||||
|
|
||||||
|
workspace_import.delay(workspace_data=file_contents)
|
||||||
|
return Response(
|
||||||
|
{"message": "Files processed.", "file_count": len(file_contents)},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from plane.license.api.views import (
|
from plane.license.api.views import (
|
||||||
InstanceEndpoint,
|
ExportWorkspaceEndpoint,
|
||||||
|
ImportWorkspaceEndpoint,
|
||||||
InstanceAdminEndpoint,
|
InstanceAdminEndpoint,
|
||||||
InstanceConfigurationEndpoint,
|
|
||||||
InstanceAdminSignInEndpoint,
|
InstanceAdminSignInEndpoint,
|
||||||
|
InstanceConfigurationEndpoint,
|
||||||
|
InstanceEndpoint,
|
||||||
SignUpScreenVisitedEndpoint,
|
SignUpScreenVisitedEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -39,4 +41,14 @@ urlpatterns = [
|
|||||||
SignUpScreenVisitedEndpoint.as_view(),
|
SignUpScreenVisitedEndpoint.as_view(),
|
||||||
name="instance-sign-up",
|
name="instance-sign-up",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"export-workspace/",
|
||||||
|
ExportWorkspaceEndpoint.as_view(),
|
||||||
|
name="workspace-exports",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"import-workspace/",
|
||||||
|
ImportWorkspaceEndpoint.as_view(),
|
||||||
|
name="workspace-imports",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
|
<html>
|
||||||
|
Hey there,<br />
|
||||||
|
Your requested url {{ url }}
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user