This commit is contained in:
DCCONSTRUCTIONS
2026-04-18 18:39:25 +03:00
commit 3ba092b60c
4944 changed files with 497564 additions and 0 deletions
+395
View File
@@ -0,0 +1,395 @@
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()