396 lines
14 KiB
Python
396 lines
14 KiB
Python
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()
|