From 298e3dc9cab15d9486ca7b48de4eaf11dcbdbbc2 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:01:22 +0530 Subject: [PATCH] [WEB-3945] chore: update workspace onboarding to add default project (#6964) * chore: add json files and initial job to push data to workspace * chore: update seed data location * chore: update seed data to use assets from static urls * chore: update seed data to use updated labels * chore: add logging and update label name * chore: add created_by for project member * chore: add created_by_id for issue user property * chore: add workspace seed task logs * chore: update log message to return task name * chore: add warning log for workspace seed task * chore: add validation for issue seed data --- apiserver/plane/app/views/workspace/base.py | 3 + .../plane/bgtasks/workspace_seed_task.py | 319 ++++++++++++++++++ apiserver/plane/seeds/data/issues.json | 85 +++++ apiserver/plane/seeds/data/labels.json | 16 + apiserver/plane/seeds/data/projects.json | 17 + apiserver/plane/seeds/data/states.json | 47 +++ apiserver/plane/settings/common.py | 3 + 7 files changed, 490 insertions(+) create mode 100644 apiserver/plane/bgtasks/workspace_seed_task.py create mode 100644 apiserver/plane/seeds/data/issues.json create mode 100644 apiserver/plane/seeds/data/labels.json create mode 100644 apiserver/plane/seeds/data/projects.json create mode 100644 apiserver/plane/seeds/data/states.json diff --git a/apiserver/plane/app/views/workspace/base.py b/apiserver/plane/app/views/workspace/base.py index c627f19b6..e92e61e51 100644 --- a/apiserver/plane/app/views/workspace/base.py +++ b/apiserver/plane/app/views/workspace/base.py @@ -42,6 +42,7 @@ from django.views.decorators.cache import cache_control from django.views.decorators.vary import vary_on_cookie from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS from plane.license.utils.instance_value import get_configuration_value +from plane.bgtasks.workspace_seed_task import workspace_seed class WorkSpaceViewSet(BaseViewSet): @@ -126,6 +127,8 @@ class WorkSpaceViewSet(BaseViewSet): data["total_members"] = total_members data["role"] = 20 + workspace_seed.delay(serializer.data["id"]) + return Response(data, status=status.HTTP_201_CREATED) return Response( [serializer.errors[error][0] for error in serializer.errors], diff --git a/apiserver/plane/bgtasks/workspace_seed_task.py b/apiserver/plane/bgtasks/workspace_seed_task.py new file mode 100644 index 000000000..c2fbfb065 --- /dev/null +++ b/apiserver/plane/bgtasks/workspace_seed_task.py @@ -0,0 +1,319 @@ +# Python imports +import os +import json +import time +import uuid +from typing import Dict +import logging + +# Django imports +from django.conf import settings + +# 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, +) + +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, + ) + + # 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={ + "group_by": None, + "order_by": "sort_order", + "type": None, + "sub_issue": True, + "show_empty_groups": True, + "layout": "list", + "calendar_date_range": "", + }, + 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], +) -> 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") + + 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(), + ) + + 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, + ) + + logger.info(f"Task: workspace_seed_task -> Issue {issue_id} created") + return + + +@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 issues + create_project_issues(workspace, project_map, state_map, label_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 diff --git a/apiserver/plane/seeds/data/issues.json b/apiserver/plane/seeds/data/issues.json new file mode 100644 index 000000000..ca341304b --- /dev/null +++ b/apiserver/plane/seeds/data/issues.json @@ -0,0 +1,85 @@ +[ + { + "id": 1, + "name": "Welcome to Plane 👋", + "sequence_id": 1, + "description_html": "
Hey there! This demo project is your playground to get hands-on with Plane. We've set this up so you can click around and see how everything works without worrying about breaking anything.
Each work item is designed to make you familiar with the basics of using Plane. Just follow along card by card at your own pace.
First thing to try
Look in the Properties section below where it says State: Todo.
Click on it and change it to Done from the dropdown. Alternatively, you can drag and drop the card to the Done column.
A Project in Plane is where all your work comes together. Think of it as a base that organizes your work items and everything else your team needs to get things done.
Note: This tutorial is already set up as a Project, and these cards you're reading are work items within it!
We're showing you how to create a new project just so you'll know exactly what to do when you're ready to start your own real one.
Look over at the left sidebar and find where it says Projects.
Hover your mouse there and you'll see a little + icon pop up - go ahead and click it!
A modal opens where you can give your project a name and other details.
Notice the Access type options? Public means anyone (except Guest users) can see and join it, while Private keeps it just for those you invite.
Tip: You can also quickly create a new project by using the keyboard shortcut P from anywhere in Plane!
Let's get your teammates on board!
First, you'll need to invite them to your workspace before they can join specific projects:
Click on your workspace name in the top-left corner, then select Settings from the dropdown.
Head over to the Members tab - this is your user management hub. Click Add member on the top right.
Enter your teammate's email address. Select a role for them (Admin, Member or Guest) that determines what they can do in the workspace.
Your team member will get an email invite. Once they've joined your workspace, you can add them to specific projects.
To do this, go to your project's Settings page.
Find the Members section, select your teammate, and assign them a project role - this controls what they can do within just this project.
That's it!
To learn more about user management, see Manage users and roles.
", + "description_stripped": "Let's get your teammates on board!First, you'll need to invite them to your workspace before they can join specific projects:Click on your workspace name in the top-left corner, then select Settings from the dropdown.Head over to the Members tab - this is your user management hub. Click Add member on the top right.Enter your teammate's email address. Select a role for them (Admin, Member or Guest) that determines what they can do in the workspace.Your team member will get an email invite. Once they've joined your workspace, you can add them to specific projects.To do this, go to your project's Settings page.Find the Members section, select your teammate, and assign them a project role - this controls what they can do within just this project.That's it!To learn more about user management, see Manage users and roles.", + "sort_order": 3000, + "state_id": 1, + "labels": [], + "priority": "none", + "project_id": 1 + }, + { + "id": 4, + "name": "3. Create and assign Work Items ✏️", + "sequence_id": 4, + "description_html": "A work item is the fundamental building block of your project. Think of these as the actionable tasks that move your project forward.
Ready to add something to your project's to-do list? Here's how:
Click the Add work item button in the top-right corner of the Work Items page.
Give your task a clear title and add any details in the description.
Set up the essentials:
Assign it to a team member (or yourself!)
Choose a priority level
Add start and due dates if there's a timeline
Tip: Save time by using the keyboard shortcut C from anywhere in your project to quickly create a new work item!
Want to dive deeper into all the things you can do with work items? Check out our documentation.
", + "description_stripped": "A work item is the fundamental building block of your project. Think of these as the actionable tasks that move your project forward.Ready to add something to your project's to-do list? Here's how:Click the Add work item button in the top-right corner of the Work Items page.Give your task a clear title and add any details in the description.Set up the essentials:Assign it to a team member (or yourself!)Choose a priority levelAdd start and due dates if there's a timelineTip: Save time by using the keyboard shortcut C from anywhere in your project to quickly create a new work item!Want to dive deeper into all the things you can do with work items? Check out our documentation.", + "sort_order": 4000, + "state_id": 1, + "labels": [2], + "priority": "none", + "project_id": 1 + }, + { + "id": 5, + "name": "4. Visualize your work 🔮", + "sequence_id": 5, + "description_html": "Plane offers multiple ways to look at your work items depending on what you need to see. Let's explore how to change views and customize them!
Look at the top toolbar in your project. You'll see several layout icons.
Click any of these icons to instantly switch between layouts.
Tip: Different layouts work best for different needs. Try Board view for tracking progress, Calendar for deadline management, and Gantt for timeline planning! See Layouts for more info.
Need to focus on specific work?
Click the Filters dropdown in the toolbar. Select criteria and choose which items to show.
Click the Display dropdown to tailor how the information appears in your layout
Created the perfect setup? Save it for later by clicking the the Save View button.
Access saved views anytime from the Views section in your sidebar.
A Cycle in Plane is like a sprint - a dedicated timeframe where your team focuses on completing specific work items. It helps you break down your project into manageable chunks with clear start and end dates so everyone knows what to work on and when it needs to be done.
Go to the Cycles section in your project (you can find it in the left sidebar)
Click the Add cycle button in the top-right corner
Enter details and set the start and end dates for your cycle.
Click Create cycle and you're ready to go!
Add existing work items to the Cycle or create new ones.
Tip: To create a new Cycle quickly, just press Q from anywhere in your project!
Want to learn more?
Starting and stopping cycles
Transferring work items between cycles
Tracking progress with charts
Check out our detailed documentation for everything you need to know!
", + "description_stripped": "A Cycle in Plane is like a sprint - a dedicated timeframe where your team focuses on completing specific work items. It helps you break down your project into manageable chunks with clear start and end dates so everyone knows what to work on and when it needs to be done.Setup CyclesGo to the Cycles section in your project (you can find it in the left sidebar)Click the Add cycle button in the top-right cornerEnter details and set the start and end dates for your cycle.Click Create cycle and you're ready to go!Add existing work items to the Cycle or create new ones.Tip: To create a new Cycle quickly, just press Q from anywhere in your project!Want to learn more?Starting and stopping cyclesTransferring work items between cyclesTracking progress with chartsCheck out our detailed documentation for everything you need to know!", + "sort_order": 6000, + "state_id": 1, + "labels": [2], + "priority": "none", + "project_id": 1 + }, + { + "id": 7, + "name": "6. Customize your settings ⚙️", + "sequence_id": 7, + "description_html": "Now that you're getting familiar with Plane, let's explore how you can customize settings to make it work just right for you and your team!
Remember those workspace settings we mentioned when inviting team members? There's a lot more you can do there:
Invite and manage workspace members
Upgrade plans and manage billing
Import data from other tools
Export your data
Manage integrations
Each project has its own settings where you can:
Change project details and visibility
Invite specific members to just this project
Customize your workflow States (like adding a \"Testing\" state)
Create and organize Labels
Enable or disable features you need (or don't need)
You can also customize your own personal experience! Click on your profile icon in the top-right corner to find:
Profile settings (update your name, photo, etc.)
Choose your timezone and preferred language for the interface
Email notification preferences (what you want to be alerted about)
Appearance settings (light/dark mode)
Taking a few minutes to set things up just the way you like will make your everyday Plane experience much smoother!
Note: Some settings are only available to workspace or project admins. If you don't see certain options, you might need admin access.