bb-plane-fork/apps/api/plane/bgtasks/workspace_seed_task.py
Nikhil c3dd790f7e
[WEB-5058] feat: enhance workspace seeding with cycles, modules, and views creation (#7951)
* feat: enhance workspace seeding with cycles, modules, and views creation

- Added `create_cycles`, `create_modules`, and `create_views` functions to the workspace seeding process, enabling the creation of cycles, modules, and views based on new seed data.
- Updated `create_project_issues` to associate issues with cycles and modules.
- Introduced new seed files: `cycles.json`, `modules.json`, and `views.json` to provide initial data for cycles, modules, and views.
- Integrated these new functionalities into the `workspace_seed` task for comprehensive workspace initialization.

* feat: add project_id to page seed data for improved association

- Added `project_id` field to the page with `id` 2 in `pages.json` to establish a clear link between pages and their respective projects.
- This enhancement supports better organization and retrieval of page data within the project context.

* feat: enhance workspace seed task with improved display properties and layout options

- Updated the `create_project_and_member` function to include new display properties and layout configurations for better project visualization.
- Modified display filters to group by state and added calendar layout options.
- Enhanced the `create_modules` and `create_views` functions with improved formatting and structure for better readability and maintainability.

* Update apps/api/plane/bgtasks/workspace_seed_task.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: correct project_id mapping in cycle and view creation functions

- Updated the `create_cycles` function to use the correct `project_id` from the `project_map` for fetching the last cycle.
- Removed redundant `view_id` extraction in the `create_views` function to streamline view creation process.

* refactor: update create_cycles function to return project mapping

- Changed the return type of the `create_cycles` function from `None` to `Dict[int, uuid.UUID]` to provide a mapping of project IDs after cycle creation.
- This modification enhances the function's utility by allowing the caller to access the generated project mappings directly.

* refactor: remove unused view_map variable in create_views function

- Eliminated the `view_map` dictionary from the `create_views` function as it was not utilized, streamlining the code.
- This change enhances code clarity and maintainability by removing unnecessary elements.

* refactor: improve issue creation logic in create_project_issues function

- Added comments to clarify the creation of issue labels, cycle issues, and module issues within the `create_project_issues` function.
- Enhanced code readability and maintainability by structuring the issue creation process with clear conditional checks for cycles and modules.

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-11 20:55:48 +05:30

517 lines
17 KiB
Python

# Python imports
import os
import json
import time
import uuid
from typing import Dict
import logging
from datetime import timedelta
# Django imports
from django.conf import settings
from django.utils import timezone
# Third party imports
from celery import shared_task
# Module imports
from plane.db.models import (
Workspace,
WorkspaceMember,
Project,
ProjectMember,
IssueUserProperty,
State,
Label,
Issue,
IssueLabel,
IssueSequence,
IssueActivity,
Page,
ProjectPage,
Cycle,
Module,
CycleIssue,
ModuleIssue,
IssueView,
)
logger = logging.getLogger("plane.worker")
def read_seed_file(filename):
"""
Read a JSON file from the seed directory.
Args:
filename (str): Name of the JSON file to read
Returns:
dict: Contents of the JSON file
"""
file_path = os.path.join(settings.SEED_DIR, "data", filename)
try:
with open(file_path, "r") as file:
return json.load(file)
except FileNotFoundError:
logger.error(f"Seed file {filename} not found in {settings.SEED_DIR}/data")
return None
except json.JSONDecodeError:
logger.error(f"Error decoding JSON from {filename}")
return None
def create_project_and_member(workspace: Workspace) -> Dict[int, uuid.UUID]:
"""Creates a project and associated members for a workspace.
Creates a new project using the workspace name and sets up all necessary
member associations and user properties.
Args:
workspace: The workspace to create the project in
Returns:
A mapping of seed project IDs to actual project IDs
"""
project_seeds = read_seed_file("projects.json")
project_identifier = "".join(ch for ch in workspace.name if ch.isalnum())[:5]
# Create members
workspace_members = WorkspaceMember.objects.filter(workspace=workspace).values("member_id", "role")
projects_map: Dict[int, uuid.UUID] = {}
if not project_seeds:
logger.warning("Task: workspace_seed_task -> No project seeds found. Skipping project creation.")
return projects_map
for project_seed in project_seeds:
project_id = project_seed.pop("id")
# Remove the name from seed data since we want to use workspace name
project_seed.pop("name", None)
project_seed.pop("identifier", None)
project = Project.objects.create(
**project_seed,
workspace=workspace,
name=workspace.name, # Use workspace name
identifier=project_identifier,
created_by_id=workspace.created_by_id,
# Enable all views in seed data
cycle_view=True,
module_view=True,
issue_views_view=True,
)
# Create project members
ProjectMember.objects.bulk_create(
[
ProjectMember(
project=project,
member_id=workspace_member["member_id"],
role=workspace_member["role"],
workspace_id=workspace.id,
created_by_id=workspace.created_by_id,
)
for workspace_member in workspace_members
]
)
# Create issue user properties
IssueUserProperty.objects.bulk_create(
[
IssueUserProperty(
project=project,
user_id=workspace_member["member_id"],
workspace_id=workspace.id,
display_filters={
"layout": "list",
"calendar": {"layout": "month", "show_weekends": False},
"group_by": "state",
"order_by": "sort_order",
"sub_issue": True,
"sub_group_by": None,
"show_empty_groups": True,
},
display_properties={
"key": True,
"link": True,
"cycle": False,
"state": True,
"labels": False,
"modules": False,
"assignee": True,
"due_date": False,
"estimate": True,
"priority": True,
"created_on": True,
"issue_type": True,
"start_date": False,
"updated_on": True,
"customer_count": True,
"sub_issue_count": False,
"attachment_count": False,
"customer_request_count": True,
},
created_by_id=workspace.created_by_id,
)
for workspace_member in workspace_members
]
)
# update map
projects_map[project_id] = project.id
logger.info(f"Task: workspace_seed_task -> Project {project_id} created")
return projects_map
def create_project_states(workspace: Workspace, project_map: Dict[int, uuid.UUID]) -> Dict[int, uuid.UUID]:
"""Creates states for each project in the workspace.
Args:
workspace: The workspace containing the projects
project_map: Mapping of seed project IDs to actual project IDs
Returns:
A mapping of seed state IDs to actual state IDs
"""
state_seeds = read_seed_file("states.json")
state_map: Dict[int, uuid.UUID] = {}
if not state_seeds:
return state_map
for state_seed in state_seeds:
state_id = state_seed.pop("id")
project_id = state_seed.pop("project_id")
state = State.objects.create(
**state_seed,
project_id=project_map[project_id],
workspace=workspace,
created_by_id=workspace.created_by_id,
)
state_map[state_id] = state.id
logger.info(f"Task: workspace_seed_task -> State {state_id} created")
return state_map
def create_project_labels(workspace: Workspace, project_map: Dict[int, uuid.UUID]) -> Dict[int, uuid.UUID]:
"""Creates labels for each project in the workspace.
Args:
workspace: The workspace containing the projects
project_map: Mapping of seed project IDs to actual project IDs
Returns:
A mapping of seed label IDs to actual label IDs
"""
label_seeds = read_seed_file("labels.json")
label_map: Dict[int, uuid.UUID] = {}
if not label_seeds:
return label_map
for label_seed in label_seeds:
label_id = label_seed.pop("id")
project_id = label_seed.pop("project_id")
label = Label.objects.create(
**label_seed,
project_id=project_map[project_id],
workspace=workspace,
created_by_id=workspace.created_by_id,
)
label_map[label_id] = label.id
logger.info(f"Task: workspace_seed_task -> Label {label_id} created")
return label_map
def create_project_issues(
workspace: Workspace,
project_map: Dict[int, uuid.UUID],
states_map: Dict[int, uuid.UUID],
labels_map: Dict[int, uuid.UUID],
cycles_map: Dict[int, uuid.UUID],
module_map: Dict[int, uuid.UUID],
) -> None:
"""Creates issues and their associated records for each project.
Creates issues along with their sequences, activities, and label associations.
Args:
workspace: The workspace containing the projects
project_map: Mapping of seed project IDs to actual project IDs
states_map: Mapping of seed state IDs to actual state IDs
labels_map: Mapping of seed label IDs to actual label IDs
"""
issue_seeds = read_seed_file("issues.json")
if not issue_seeds:
return
for issue_seed in issue_seeds:
required_fields = ["id", "labels", "project_id", "state_id"]
# get the values
for field in required_fields:
if field not in issue_seed:
logger.error(f"Task: workspace_seed_task -> Required field '{field}' missing in issue seed")
continue
# get the values
issue_id = issue_seed.pop("id")
labels = issue_seed.pop("labels")
project_id = issue_seed.pop("project_id")
state_id = issue_seed.pop("state_id")
cycle_id = issue_seed.pop("cycle_id")
module_ids = issue_seed.pop("module_ids")
issue = Issue.objects.create(
**issue_seed,
state_id=states_map[state_id],
project_id=project_map[project_id],
workspace=workspace,
created_by_id=workspace.created_by_id,
)
IssueSequence.objects.create(
issue=issue,
project_id=project_map[project_id],
workspace_id=workspace.id,
created_by_id=workspace.created_by_id,
)
IssueActivity.objects.create(
issue=issue,
project_id=project_map[project_id],
workspace_id=workspace.id,
comment="created the issue",
verb="created",
actor_id=workspace.created_by_id,
epoch=time.time(),
)
# Create issue labels
for label_id in labels:
IssueLabel.objects.create(
issue=issue,
label_id=labels_map[label_id],
project_id=project_map[project_id],
workspace_id=workspace.id,
created_by_id=workspace.created_by_id,
)
# Create cycle issues
if cycle_id:
CycleIssue.objects.create(
issue=issue,
cycle_id=cycles_map[cycle_id],
project_id=project_map[project_id],
workspace_id=workspace.id,
created_by_id=workspace.created_by_id,
)
# Create module issues
if module_ids:
for module_id in module_ids:
ModuleIssue.objects.create(
issue=issue,
module_id=module_map[module_id],
project_id=project_map[project_id],
workspace_id=workspace.id,
created_by_id=workspace.created_by_id,
)
logger.info(f"Task: workspace_seed_task -> Issue {issue_id} created")
return
def create_pages(workspace: Workspace, project_map: Dict[int, uuid.UUID]) -> None:
"""Creates pages for each project in the workspace.
Args:
workspace: The workspace containing the projects
project_map: Mapping of seed project IDs to actual project IDs
"""
page_seeds = read_seed_file("pages.json")
if not page_seeds:
return
for page_seed in page_seeds:
page_id = page_seed.pop("id")
page = Page.objects.create(
workspace_id=workspace.id,
is_global=False,
access=page_seed.get("access", Page.PUBLIC_ACCESS),
name=page_seed.get("name"),
description=page_seed.get("description", {}),
description_html=page_seed.get("description_html", "<p></p>"),
description_binary=page_seed.get("description_binary", None),
description_stripped=page_seed.get("description_stripped", None),
created_by_id=workspace.created_by_id,
updated_by_id=workspace.created_by_id,
owned_by_id=workspace.created_by_id,
)
logger.info(f"Task: workspace_seed_task -> Page {page_id} created")
if page_seed.get("project_id") and page_seed.get("type") == "PROJECT":
ProjectPage.objects.create(
workspace_id=workspace.id,
project_id=project_map[page_seed.get("project_id")],
page_id=page.id,
created_by_id=workspace.created_by_id,
updated_by_id=workspace.created_by_id,
)
logger.info(f"Task: workspace_seed_task -> Project Page {page_id} created")
return
def create_cycles(workspace: Workspace, project_map: Dict[int, uuid.UUID]) -> Dict[int, uuid.UUID]:
# Create cycles
cycle_seeds = read_seed_file("cycles.json")
if not cycle_seeds:
return
cycle_map: Dict[int, uuid.UUID] = {}
for cycle_seed in cycle_seeds:
cycle_id = cycle_seed.pop("id")
project_id = cycle_seed.pop("project_id")
type = cycle_seed.pop("type")
if type == "CURRENT":
start_date = timezone.now()
end_date = start_date + timedelta(days=14)
if type == "UPCOMING":
# Get the last cycle
last_cycle = Cycle.objects.filter(project_id=project_map[project_id]).order_by("-end_date").first()
if last_cycle:
start_date = last_cycle.end_date + timedelta(days=1)
end_date = start_date + timedelta(days=14)
else:
start_date = timezone.now() + timedelta(days=14)
end_date = start_date + timedelta(days=14)
cycle = Cycle.objects.create(
**cycle_seed,
start_date=start_date,
end_date=end_date,
project_id=project_map[project_id],
workspace=workspace,
created_by_id=workspace.created_by_id,
owned_by_id=workspace.created_by_id,
)
cycle_map[cycle_id] = cycle.id
logger.info(f"Task: workspace_seed_task -> Cycle {cycle_id} created")
return cycle_map
def create_modules(workspace: Workspace, project_map: Dict[int, uuid.UUID]) -> None:
"""Creates modules for each project in the workspace.
Args:
workspace: The workspace containing the projects
project_map: Mapping of seed project IDs to actual project IDs
"""
module_seeds = read_seed_file("modules.json")
if not module_seeds:
return
module_map: Dict[int, uuid.UUID] = {}
for index, module_seed in enumerate(module_seeds):
module_id = module_seed.pop("id")
project_id = module_seed.pop("project_id")
start_date = timezone.now() + timedelta(days=index * 2)
end_date = start_date + timedelta(days=14)
module = Module.objects.create(
**module_seed,
start_date=start_date,
target_date=end_date,
project_id=project_map[project_id],
workspace=workspace,
created_by_id=workspace.created_by_id,
)
module_map[module_id] = module.id
logger.info(f"Task: workspace_seed_task -> Module {module_id} created")
return module_map
def create_views(workspace: Workspace, project_map: Dict[int, uuid.UUID]) -> None:
"""Creates views for each project in the workspace.
Args:
workspace: The workspace containing the projects
project_map: Mapping of seed project IDs to actual project IDs
"""
view_seeds = read_seed_file("views.json")
if not view_seeds:
return
for view_seed in view_seeds:
project_id = view_seed.pop("project_id")
IssueView.objects.create(
**view_seed,
project_id=project_map[project_id],
workspace=workspace,
created_by_id=workspace.created_by_id,
owned_by_id=workspace.created_by_id,
)
@shared_task
def workspace_seed(workspace_id: uuid.UUID) -> None:
"""Seeds a new workspace with initial project data.
Creates a complete workspace setup including:
- Projects and project members
- Project states
- Project labels
- Issues and their associations
Args:
workspace_id: ID of the workspace to seed
"""
try:
logger.info(f"Task: workspace_seed_task -> Seeding workspace {workspace_id}")
# Get the workspace
workspace = Workspace.objects.get(id=workspace_id)
# Create a project with the same name as workspace
project_map = create_project_and_member(workspace)
# Create project states
state_map = create_project_states(workspace, project_map)
# Create project labels
label_map = create_project_labels(workspace, project_map)
# Create project cycles
cycle_map = create_cycles(workspace, project_map)
# Create project modules
module_map = create_modules(workspace, project_map)
# create project issues
create_project_issues(workspace, project_map, state_map, label_map, cycle_map, module_map)
# create project views
create_views(workspace, project_map)
# create project pages
create_pages(workspace, project_map)
logger.info(f"Task: workspace_seed_task -> Workspace {workspace_id} seeded successfully")
return
except Exception as e:
logger.error(f"Task: workspace_seed_task -> Failed to seed workspace {workspace_id}: {str(e)}")
raise e