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": "

Подготовить короткую сверку по основным контрагентам за март.

", "priority": "high", "state_group": "unstarted", "assignees": ["accountant@nodedc.local"], "start_date": TODAY, "target_date": TODAY + timedelta(days=2), }, { "project": "Менеджеры", "name": "Подготовить недельный статус по команде продаж", "description_html": "

Общая задача для контура менеджеров.

Нужно собрать краткий weekly update по активным клиентам и рискам.

", "priority": "medium", "state_group": "backlog", "assignees": [], "start_date": TODAY, "target_date": None, }, { "project": "Запросы документов", "name": "Запросить закрывающие документы по договору ND-24", "description_html": "

Связаться с контрагентом и запросить закрывающие документы.

Шаблон письма приложен вложением.

", "priority": "urgent", "state_group": "started", "assignees": ["docs@nodedc.local"], "start_date": TODAY, "target_date": TODAY + timedelta(days=1), "attachment": True, }, { "project": "Бухгалтерия", "name": "Согласовать лимиты расходов на май", "description_html": "

Подготовить лимиты и согласовать их с ответственными менеджерами до конца недели.

", "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()