NODEDC_TASKMANAGER/scripts/bootstrap_nodedc_demo.py

396 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from datetime import date, timedelta
from pathlib import Path
from uuid import uuid4
from django.db import transaction
from plane.db.models import (
DEFAULT_STATES,
FileAsset,
Issue,
IssueAssignee,
IssueView,
Profile,
Project,
ProjectMember,
State,
User,
Workspace,
WorkspaceMember,
)
from plane.license.models import Instance, InstanceAdmin
from plane.settings.storage import S3Storage
PASSWORD = "NodeDC123!"
TODAY = date.today()
ATTACHMENT_PATH = Path("/tmp/nodedc-document-request-template.txt")
USERS = [
{
"email": "admin@nodedc.local",
"username": "admin_nodedc",
"first_name": "Алексей",
"last_name": "Админ",
"display_name": "Алексей Админ",
"workspace_role": 20,
},
{
"email": "accountant@nodedc.local",
"username": "accountant_nodedc",
"first_name": "Борис",
"last_name": "Бухгалтер",
"display_name": "Борис Бухгалтер",
"workspace_role": 15,
},
{
"email": "manager@nodedc.local",
"username": "manager_nodedc",
"first_name": "Марина",
"last_name": "Менеджер",
"display_name": "Марина Менеджер",
"workspace_role": 15,
},
{
"email": "docs@nodedc.local",
"username": "docs_nodedc",
"first_name": "Дарья",
"last_name": "Документы",
"display_name": "Дарья Документы",
"workspace_role": 15,
},
]
PROJECTS = [
{
"name": "Бухгалтерия",
"identifier": "BUH",
"description": "Контур для финансовых задач, сверок и согласований расходов.",
"members": ["admin@nodedc.local", "accountant@nodedc.local"],
},
{
"name": "Менеджеры",
"identifier": "MGR",
"description": "Контур для менеджерских задач, общих поручений и статусов.",
"members": ["admin@nodedc.local", "manager@nodedc.local"],
},
{
"name": "Запросы документов",
"identifier": "DOC",
"description": "Контур для входящих и исходящих запросов документов, актов и счетов.",
"members": ["admin@nodedc.local", "docs@nodedc.local", "accountant@nodedc.local"],
},
]
STATE_TEMPLATES = [
{"group": "backlog", "name": "Бэклог", "color": "#60646C", "default": True},
{"group": "unstarted", "name": "К выполнению", "color": "#60646C", "default": False},
{"group": "started", "name": "В работе", "color": "#F59E0B", "default": False},
{"group": "completed", "name": "Готово", "color": "#46A758", "default": False},
{"group": "cancelled", "name": "Отменено", "color": "#9AA4BC", "default": False},
{"group": "triage", "name": "Триаж", "color": "#4E5355", "default": False},
]
ISSUES = [
{
"project": "Бухгалтерия",
"name": "Подготовить сверку с контрагентами за март",
"description_html": "<p>Подготовить короткую сверку по основным контрагентам за март.</p><ul><li>Проверить закрывающие документы</li><li>Сверить суммы</li><li>Отметить расхождения</li></ul>",
"priority": "high",
"state_group": "unstarted",
"assignees": ["accountant@nodedc.local"],
"start_date": TODAY,
"target_date": TODAY + timedelta(days=2),
},
{
"project": "Менеджеры",
"name": "Подготовить недельный статус по команде продаж",
"description_html": "<p>Общая задача для контура менеджеров.</p><p>Нужно собрать краткий weekly update по активным клиентам и рискам.</p>",
"priority": "medium",
"state_group": "backlog",
"assignees": [],
"start_date": TODAY,
"target_date": None,
},
{
"project": "Запросы документов",
"name": "Запросить закрывающие документы по договору ND-24",
"description_html": "<p>Связаться с контрагентом и запросить закрывающие документы.</p><ul><li>Акт</li><li>Счёт</li><li>Подписанный договор</li></ul><p>Шаблон письма приложен вложением.</p>",
"priority": "urgent",
"state_group": "started",
"assignees": ["docs@nodedc.local"],
"start_date": TODAY,
"target_date": TODAY + timedelta(days=1),
"attachment": True,
},
{
"project": "Бухгалтерия",
"name": "Согласовать лимиты расходов на май",
"description_html": "<p>Подготовить лимиты и согласовать их с ответственными менеджерами до конца недели.</p>",
"priority": "high",
"state_group": "unstarted",
"assignees": ["admin@nodedc.local", "manager@nodedc.local"],
"start_date": TODAY,
"target_date": TODAY + timedelta(days=5),
},
]
def ensure_user(spec):
user = User.objects.filter(email=spec["email"]).first()
if user is None:
user = User.objects.create(email=spec["email"], username=spec["username"])
user.username = spec["username"]
user.first_name = spec["first_name"]
user.last_name = spec["last_name"]
user.display_name = spec["display_name"]
user.is_active = True
user.is_email_verified = True
user.user_timezone = "Europe/Moscow"
user.is_staff = spec["workspace_role"] == 20
user.set_password(PASSWORD)
user.save()
profile, _ = Profile.objects.get_or_create(user=user)
profile.language = "ru"
profile.is_onboarded = True
profile.company_name = "NodeDC"
profile.onboarding_step = {
"profile_complete": True,
"workspace_create": True,
"workspace_invite": True,
"workspace_join": True,
}
profile.save()
return user, profile
def ensure_workspace(admin_user, user_map):
workspace, _ = Workspace.objects.get_or_create(
slug="nodedc",
defaults={
"name": "NodeDC",
"owner": admin_user,
"timezone": "Europe/Moscow",
},
)
workspace.name = "NodeDC"
workspace.owner = admin_user
workspace.timezone = "Europe/Moscow"
workspace.organization_size = "11-50"
workspace.save()
for spec in USERS:
member, _ = WorkspaceMember.objects.get_or_create(
workspace=workspace,
member=user_map[spec["email"]],
defaults={"role": spec["workspace_role"], "company_role": spec["display_name"]},
)
member.role = spec["workspace_role"]
member.company_role = spec["display_name"]
member.is_active = True
member.save()
for user in user_map.values():
profile = Profile.objects.get(user=user)
profile.last_workspace_id = workspace.id
profile.save(update_fields=["last_workspace_id"])
return workspace
def ensure_project(workspace, admin_user, user_map, spec):
project, _ = Project.objects.get_or_create(
workspace=workspace,
identifier=spec["identifier"],
defaults={
"name": spec["name"],
"description": spec["description"],
"network": 0,
"project_lead": admin_user,
},
)
project.name = spec["name"]
project.description = spec["description"]
project.network = 0
project.project_lead = admin_user
project.timezone = "Europe/Moscow"
project.save()
for idx, state_spec in enumerate(STATE_TEMPLATES):
state = State.all_state_objects.filter(project=project, group=state_spec["group"]).first()
if state is None:
state = State(project=project, workspace=workspace)
state.name = state_spec["name"]
state.group = state_spec["group"]
state.color = state_spec["color"]
state.default = state_spec["default"]
state.is_triage = state_spec["group"] == "triage"
state.sequence = DEFAULT_STATES[idx]["sequence"]
state.created_by = admin_user
state.updated_by = admin_user
state.save(disable_auto_set_user=True)
if state_spec["default"]:
project.default_state = state
project.save()
for email in spec["members"]:
project_member, _ = ProjectMember.objects.get_or_create(
project=project,
member=user_map[email],
defaults={
"workspace": workspace,
"role": 20 if email == "admin@nodedc.local" else 15,
},
)
project_member.workspace = workspace
project_member.role = 20 if email == "admin@nodedc.local" else 15
project_member.is_active = True
project_member.save()
return project
def ensure_issue(workspace, project, admin_user, user_map, spec):
state = State.all_state_objects.get(project=project, group=spec["state_group"])
issue, created = Issue.objects.get_or_create(
project=project,
workspace=workspace,
name=spec["name"],
defaults={
"state": state,
"priority": spec["priority"],
"start_date": spec["start_date"],
"target_date": spec["target_date"],
"description_html": spec["description_html"],
"created_by": admin_user,
"updated_by": admin_user,
},
)
issue.state = state
issue.priority = spec["priority"]
issue.start_date = spec["start_date"]
issue.target_date = spec["target_date"]
issue.description_html = spec["description_html"]
issue.created_by = admin_user
issue.updated_by = admin_user
issue.save(disable_auto_set_user=True)
IssueAssignee.objects.filter(issue=issue).delete()
for email in spec["assignees"]:
IssueAssignee.objects.get_or_create(
issue=issue,
assignee=user_map[email],
defaults={
"project": project,
"workspace": workspace,
"created_by": admin_user,
"updated_by": admin_user,
},
)
legacy_attachments = issue.issue_attachment.all()
if legacy_attachments.exists():
legacy_attachments.delete()
has_visible_attachment = FileAsset.objects.filter(
issue=issue,
workspace=workspace,
project=project,
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
is_uploaded=True,
).exists()
if spec.get("attachment") and ATTACHMENT_PATH.exists() and not has_visible_attachment:
object_name = f"nodedc/{uuid4().hex[:12]}-doc-template.txt"
storage = S3Storage()
with ATTACHMENT_PATH.open("rb") as file_obj:
uploaded = storage.upload_file(file_obj, object_name, content_type="text/plain")
if not uploaded:
raise RuntimeError(f"Failed to upload demo attachment to object storage: {object_name}")
FileAsset.objects.create(
attributes={
"name": ATTACHMENT_PATH.name,
"type": "text/plain",
"size": ATTACHMENT_PATH.stat().st_size,
},
asset=object_name,
size=ATTACHMENT_PATH.stat().st_size,
user=admin_user,
workspace=workspace,
project=project,
issue=issue,
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
is_uploaded=True,
created_by=admin_user,
updated_by=admin_user,
)
return issue, created
def ensure_view(workspace, project, admin_user):
view, _ = IssueView.objects.get_or_create(
workspace=workspace,
project=project,
name="Срочные документы",
defaults={
"description": "Быстрый просмотр срочных запросов документов",
"filters": {"priority": ["urgent", "high"]},
"rich_filters": {},
"owned_by": admin_user,
"access": 1,
"logo_props": {},
},
)
view.description = "Быстрый просмотр срочных запросов документов"
view.filters = {"priority": ["urgent", "high"]}
view.rich_filters = {}
view.owned_by = admin_user
view.access = 1
view.save(disable_auto_set_user=True)
return view
@transaction.atomic
def main():
if ATTACHMENT_PATH.exists() is False:
raise FileNotFoundError(f"Attachment file not found: {ATTACHMENT_PATH}")
user_map = {}
for spec in USERS:
user, _ = ensure_user(spec)
user_map[spec["email"]] = user
admin_user = user_map["admin@nodedc.local"]
instance = Instance.objects.first()
if instance is None:
raise RuntimeError("Plane instance is not initialized")
instance.instance_name = "NodeDC Plane PoC"
instance.domain = "http://localhost:8090"
instance.is_setup_done = True
instance.is_signup_screen_visited = True
instance.save(update_fields=["instance_name", "domain", "is_setup_done", "is_signup_screen_visited", "updated_at"])
InstanceAdmin.objects.get_or_create(user=admin_user, instance=instance, defaults={"role": 20, "is_verified": True})
workspace = ensure_workspace(admin_user, user_map)
project_map = {}
for project_spec in PROJECTS:
project_map[project_spec["name"]] = ensure_project(workspace, admin_user, user_map, project_spec)
for issue_spec in ISSUES:
ensure_issue(workspace, project_map[issue_spec["project"]], admin_user, user_map, issue_spec)
ensure_view(workspace, project_map["Запросы документов"], admin_user)
summary = {
"workspace": workspace.slug,
"users": [spec["email"] for spec in USERS],
"projects": [spec["name"] for spec in PROJECTS],
"issues": [spec["name"] for spec in ISSUES],
"default_password": PASSWORD,
}
print(summary)
main()