chore: issue version migrations updates

This commit is contained in:
sriram veeraghanta 2024-11-28 12:42:30 +05:30
parent d2758fe5e6
commit 0d70397639
14 changed files with 473 additions and 281 deletions

View file

@ -13,7 +13,6 @@ from .user import (
from .workspace import ( from .workspace import (
WorkSpaceSerializer, WorkSpaceSerializer,
WorkSpaceMemberSerializer, WorkSpaceMemberSerializer,
TeamSerializer,
WorkSpaceMemberInviteSerializer, WorkSpaceMemberInviteSerializer,
WorkspaceLiteSerializer, WorkspaceLiteSerializer,
WorkspaceThemeSerializer, WorkspaceThemeSerializer,

View file

@ -6,11 +6,8 @@ from .base import BaseSerializer, DynamicBaseSerializer
from .user import UserLiteSerializer, UserAdminLiteSerializer from .user import UserLiteSerializer, UserAdminLiteSerializer
from plane.db.models import ( from plane.db.models import (
User,
Workspace, Workspace,
WorkspaceMember, WorkspaceMember,
Team,
TeamMember,
WorkspaceMemberInvite, WorkspaceMemberInvite,
WorkspaceTheme, WorkspaceTheme,
WorkspaceUserProperties, WorkspaceUserProperties,
@ -97,52 +94,6 @@ class WorkSpaceMemberInviteSerializer(BaseSerializer):
] ]
class TeamSerializer(BaseSerializer):
members_detail = UserLiteSerializer(read_only=True, source="members", many=True)
members = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
write_only=True,
required=False,
)
class Meta:
model = Team
fields = "__all__"
read_only_fields = [
"workspace",
"created_by",
"updated_by",
"created_at",
"updated_at",
]
def create(self, validated_data, **kwargs):
if "members" in validated_data:
members = validated_data.pop("members")
workspace = self.context["workspace"]
team = Team.objects.create(**validated_data, workspace=workspace)
team_members = [
TeamMember(member=member, team=team, workspace=workspace)
for member in members
]
TeamMember.objects.bulk_create(team_members, batch_size=10)
return team
team = Team.objects.create(**validated_data)
return team
def update(self, instance, validated_data):
if "members" in validated_data:
members = validated_data.pop("members")
TeamMember.objects.filter(team=instance).delete()
team_members = [
TeamMember(member=member, team=instance, workspace=instance.workspace)
for member in members
]
TeamMember.objects.bulk_create(team_members, batch_size=10)
return super().update(instance, validated_data)
return super().update(instance, validated_data)
class WorkspaceThemeSerializer(BaseSerializer): class WorkspaceThemeSerializer(BaseSerializer):
class Meta: class Meta:
model = WorkspaceTheme model = WorkspaceTheme

View file

@ -7,7 +7,6 @@ from plane.app.views import (
ProjectMemberViewSet, ProjectMemberViewSet,
ProjectMemberUserEndpoint, ProjectMemberUserEndpoint,
ProjectJoinEndpoint, ProjectJoinEndpoint,
AddTeamToProjectEndpoint,
ProjectUserViewsEndpoint, ProjectUserViewsEndpoint,
ProjectIdentifierEndpoint, ProjectIdentifierEndpoint,
ProjectFavoritesViewSet, ProjectFavoritesViewSet,
@ -83,11 +82,6 @@ urlpatterns = [
ProjectMemberViewSet.as_view({"post": "leave"}), ProjectMemberViewSet.as_view({"post": "leave"}),
name="project-member", name="project-member",
), ),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/team-invite/",
AddTeamToProjectEndpoint.as_view(),
name="projects",
),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-views/", "workspaces/<str:slug>/projects/<uuid:project_id>/project-views/",
ProjectUserViewsEndpoint.as_view(), ProjectUserViewsEndpoint.as_view(),

View file

@ -10,7 +10,6 @@ from plane.app.views import (
WorkspaceMemberUserEndpoint, WorkspaceMemberUserEndpoint,
WorkspaceMemberUserViewsEndpoint, WorkspaceMemberUserViewsEndpoint,
WorkSpaceAvailabilityCheckEndpoint, WorkSpaceAvailabilityCheckEndpoint,
TeamMemberViewSet,
UserLastProjectWithWorkspaceEndpoint, UserLastProjectWithWorkspaceEndpoint,
WorkspaceThemeViewSet, WorkspaceThemeViewSet,
WorkspaceUserProfileStatsEndpoint, WorkspaceUserProfileStatsEndpoint,
@ -69,7 +68,9 @@ urlpatterns = [
# user workspace invitations # user workspace invitations
path( path(
"users/me/workspaces/invitations/", "users/me/workspaces/invitations/",
UserWorkspaceInvitationsViewSet.as_view({"get": "list", "post": "create"}), UserWorkspaceInvitationsViewSet.as_view(
{"get": "list", "post": "create"}
),
name="user-workspace-invitations", name="user-workspace-invitations",
), ),
path( path(
@ -100,23 +101,6 @@ urlpatterns = [
WorkSpaceMemberViewSet.as_view({"post": "leave"}), WorkSpaceMemberViewSet.as_view({"post": "leave"}),
name="leave-workspace-members", name="leave-workspace-members",
), ),
path(
"workspaces/<str:slug>/teams/",
TeamMemberViewSet.as_view({"get": "list", "post": "create"}),
name="workspace-team-members",
),
path(
"workspaces/<str:slug>/teams/<uuid:pk>/",
TeamMemberViewSet.as_view(
{
"put": "update",
"patch": "partial_update",
"delete": "destroy",
"get": "retrieve",
}
),
name="workspace-team-members",
),
path( path(
"users/last-visited-workspace/", "users/last-visited-workspace/",
UserLastProjectWithWorkspaceEndpoint.as_view(), UserLastProjectWithWorkspaceEndpoint.as_view(),

View file

@ -16,7 +16,6 @@ from .project.invite import (
from .project.member import ( from .project.member import (
ProjectMemberViewSet, ProjectMemberViewSet,
AddTeamToProjectEndpoint,
ProjectMemberUserEndpoint, ProjectMemberUserEndpoint,
UserProjectRolesEndpoint, UserProjectRolesEndpoint,
) )
@ -49,7 +48,6 @@ from .workspace.favorite import (
from .workspace.member import ( from .workspace.member import (
WorkSpaceMemberViewSet, WorkSpaceMemberViewSet,
TeamMemberViewSet,
WorkspaceMemberUserEndpoint, WorkspaceMemberUserEndpoint,
WorkspaceProjectMemberEndpoint, WorkspaceProjectMemberEndpoint,
WorkspaceMemberUserViewsEndpoint, WorkspaceMemberUserViewsEndpoint,
@ -88,8 +86,6 @@ from .cycle.base import (
CycleFavoriteViewSet, CycleFavoriteViewSet,
TransferCycleIssueEndpoint, TransferCycleIssueEndpoint,
CycleUserPropertiesEndpoint, CycleUserPropertiesEndpoint,
CycleViewSet,
TransferCycleIssueEndpoint,
CycleAnalyticsEndpoint, CycleAnalyticsEndpoint,
CycleProgressEndpoint, CycleProgressEndpoint,
) )
@ -206,6 +202,5 @@ from .dashboard.base import DashboardEndpoint, WidgetsEndpoint
from .error_404 import custom_404_view from .error_404 import custom_404_view
from .exporter.base import ExportIssuesEndpoint
from .notification.base import MarkAllReadNotificationViewSet from .notification.base import MarkAllReadNotificationViewSet
from .user.base import AccountEndpoint, ProfileEndpoint, UserSessionEndpoint from .user.base import AccountEndpoint, ProfileEndpoint, UserSessionEndpoint

View file

@ -11,7 +11,6 @@ from plane.app.serializers import (
) )
from plane.app.permissions import ( from plane.app.permissions import (
ProjectBasePermission,
ProjectMemberPermission, ProjectMemberPermission,
ProjectLitePermission, ProjectLitePermission,
WorkspaceUserPermission, WorkspaceUserPermission,
@ -20,8 +19,6 @@ from plane.app.permissions import (
from plane.db.models import ( from plane.db.models import (
Project, Project,
ProjectMember, ProjectMember,
Workspace,
TeamMember,
IssueUserProperty, IssueUserProperty,
WorkspaceMember, WorkspaceMember,
) )
@ -86,7 +83,10 @@ class ProjectMemberViewSet(BaseViewSet):
workspace_member_role = WorkspaceMember.objects.get( workspace_member_role = WorkspaceMember.objects.get(
workspace__slug=slug, member=member, is_active=True workspace__slug=slug, member=member, is_active=True
).role ).role
if workspace_member_role in [20] and member_roles.get(member) in [5, 15]: if workspace_member_role in [20] and member_roles.get(member) in [
5,
15,
]:
return Response( return Response(
{ {
"error": "You cannot add a user with role lower than the workspace role" "error": "You cannot add a user with role lower than the workspace role"
@ -94,7 +94,10 @@ class ProjectMemberViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
if workspace_member_role in [5] and member_roles.get(member) in [15, 20]: if workspace_member_role in [5] and member_roles.get(member) in [
15,
20,
]:
return Response( return Response(
{ {
"error": "You cannot add a user with role higher than the workspace role" "error": "You cannot add a user with role higher than the workspace role"
@ -132,7 +135,8 @@ class ProjectMemberViewSet(BaseViewSet):
sort_order = [ sort_order = [
project_member.get("sort_order") project_member.get("sort_order")
for project_member in project_members for project_member in project_members
if str(project_member.get("member_id")) == str(member.get("member_id")) if str(project_member.get("member_id"))
== str(member.get("member_id"))
] ]
# Create a new project member # Create a new project member
bulk_project_members.append( bulk_project_members.append(
@ -141,7 +145,9 @@ class ProjectMemberViewSet(BaseViewSet):
role=member.get("role", 5), role=member.get("role", 5),
project_id=project_id, project_id=project_id,
workspace_id=project.workspace_id, workspace_id=project.workspace_id,
sort_order=(sort_order[0] - 10000 if len(sort_order) else 65535), sort_order=(
sort_order[0] - 10000 if len(sort_order) else 65535
),
) )
) )
# Create a new issue property # Create a new issue property
@ -232,7 +238,9 @@ class ProjectMemberViewSet(BaseViewSet):
> requested_project_member.role > requested_project_member.role
): ):
return Response( return Response(
{"error": "You cannot update a role that is higher than your own role"}, {
"error": "You cannot update a role that is higher than your own role"
},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
@ -272,7 +280,9 @@ class ProjectMemberViewSet(BaseViewSet):
# User cannot deactivate higher role # User cannot deactivate higher role
if requesting_project_member.role < project_member.role: if requesting_project_member.role < project_member.role:
return Response( return Response(
{"error": "You cannot remove a user having role higher than you"}, {
"error": "You cannot remove a user having role higher than you"
},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
@ -293,7 +303,10 @@ class ProjectMemberViewSet(BaseViewSet):
if ( if (
project_member.role == 20 project_member.role == 20
and not ProjectMember.objects.filter( and not ProjectMember.objects.filter(
workspace__slug=slug, project_id=project_id, role=20, is_active=True workspace__slug=slug,
project_id=project_id,
role=20,
is_active=True,
).count() ).count()
> 1 > 1
): ):
@ -309,53 +322,6 @@ class ProjectMemberViewSet(BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
class AddTeamToProjectEndpoint(BaseAPIView):
permission_classes = [ProjectBasePermission]
def post(self, request, slug, project_id):
team_members = TeamMember.objects.filter(
workspace__slug=slug, team__in=request.data.get("teams", [])
).values_list("member", flat=True)
if len(team_members) == 0:
return Response(
{"error": "No such team exists"}, status=status.HTTP_400_BAD_REQUEST
)
workspace = Workspace.objects.get(slug=slug)
project_members = []
issue_props = []
for member in team_members:
project_members.append(
ProjectMember(
project_id=project_id,
member_id=member,
workspace=workspace,
created_by=request.user,
)
)
issue_props.append(
IssueUserProperty(
project_id=project_id,
user_id=member,
workspace=workspace,
created_by=request.user,
)
)
ProjectMember.objects.bulk_create(
project_members, batch_size=10, ignore_conflicts=True
)
_ = IssueUserProperty.objects.bulk_create(
issue_props, batch_size=10, ignore_conflicts=True
)
serializer = ProjectMemberSerializer(project_members, many=True)
return Response(serializer.data, status=status.HTTP_201_CREATED)
class ProjectMemberUserEndpoint(BaseAPIView): class ProjectMemberUserEndpoint(BaseAPIView):
def get(self, request, slug, project_id): def get(self, request, slug, project_id):
project_member = ProjectMember.objects.get( project_member = ProjectMember.objects.get(
@ -378,6 +344,7 @@ class UserProjectRolesEndpoint(BaseAPIView):
).values("project_id", "role") ).values("project_id", "role")
project_members = { project_members = {
str(member["project_id"]): member["role"] for member in project_members str(member["project_id"]): member["role"]
for member in project_members
} }
return Response(project_members, status=status.HTTP_200_OK) return Response(project_members, status=status.HTTP_200_OK)

View file

@ -1,14 +1,18 @@
# Django imports # Django imports
from django.db.models import CharField, Count, Q, OuterRef, Subquery, IntegerField from django.db.models import (
Count,
Q,
OuterRef,
Subquery,
IntegerField,
)
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.db.models.functions import Cast
# Third party modules # Third party modules
from rest_framework import status from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
from plane.app.permissions import ( from plane.app.permissions import (
WorkSpaceAdminPermission,
WorkspaceEntityPermission, WorkspaceEntityPermission,
allow_permission, allow_permission,
ROLE, ROLE,
@ -17,8 +21,6 @@ from plane.app.permissions import (
# Module imports # Module imports
from plane.app.serializers import ( from plane.app.serializers import (
ProjectMemberRoleSerializer, ProjectMemberRoleSerializer,
TeamSerializer,
UserLiteSerializer,
WorkspaceMemberAdminSerializer, WorkspaceMemberAdminSerializer,
WorkspaceMemberMeSerializer, WorkspaceMemberMeSerializer,
WorkSpaceMemberSerializer, WorkSpaceMemberSerializer,
@ -27,9 +29,6 @@ from plane.app.views.base import BaseAPIView
from plane.db.models import ( from plane.db.models import (
Project, Project,
ProjectMember, ProjectMember,
Team,
User,
Workspace,
WorkspaceMember, WorkspaceMember,
DraftIssue, DraftIssue,
) )
@ -120,7 +119,9 @@ class WorkSpaceMemberViewSet(BaseViewSet):
if requesting_workspace_member.role < workspace_member.role: if requesting_workspace_member.role < workspace_member.role:
return Response( return Response(
{"error": "You cannot remove a user having role higher than you"}, {
"error": "You cannot remove a user having role higher than you"
},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
@ -147,7 +148,9 @@ class WorkSpaceMemberViewSet(BaseViewSet):
# Deactivate the users from the projects where the user is part of # Deactivate the users from the projects where the user is part of
_ = ProjectMember.objects.filter( _ = ProjectMember.objects.filter(
workspace__slug=slug, member_id=workspace_member.member_id, is_active=True workspace__slug=slug,
member_id=workspace_member.member_id,
is_active=True,
).update(is_active=False) ).update(is_active=False)
workspace_member.is_active = False workspace_member.is_active = False
@ -161,7 +164,9 @@ class WorkSpaceMemberViewSet(BaseViewSet):
multiple=True, multiple=True,
) )
@invalidate_cache(path="/api/users/me/settings/") @invalidate_cache(path="/api/users/me/settings/")
@invalidate_cache(path="api/users/me/workspaces/", user=False, multiple=True) @invalidate_cache(
path="api/users/me/workspaces/", user=False, multiple=True
)
@allow_permission( @allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE" allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
) )
@ -208,7 +213,9 @@ class WorkSpaceMemberViewSet(BaseViewSet):
# # Deactivate the users from the projects where the user is part of # # Deactivate the users from the projects where the user is part of
_ = ProjectMember.objects.filter( _ = ProjectMember.objects.filter(
workspace__slug=slug, member_id=workspace_member.member_id, is_active=True workspace__slug=slug,
member_id=workspace_member.member_id,
is_active=True,
).update(is_active=False) ).update(is_active=False)
# # Deactivate the user # # Deactivate the user
@ -272,7 +279,9 @@ class WorkspaceProjectMemberEndpoint(BaseAPIView):
project_members = ProjectMember.objects.filter( project_members = ProjectMember.objects.filter(
workspace__slug=slug, project_id__in=project_ids, is_active=True workspace__slug=slug, project_id__in=project_ids, is_active=True
).select_related("project", "member", "workspace") ).select_related("project", "member", "workspace")
project_members = ProjectMemberRoleSerializer(project_members, many=True).data project_members = ProjectMemberRoleSerializer(
project_members, many=True
).data
project_members_dict = dict() project_members_dict = dict()
@ -284,53 +293,3 @@ class WorkspaceProjectMemberEndpoint(BaseAPIView):
project_members_dict[str(project_id)].append(project_member) project_members_dict[str(project_id)].append(project_member)
return Response(project_members_dict, status=status.HTTP_200_OK) return Response(project_members_dict, status=status.HTTP_200_OK)
class TeamMemberViewSet(BaseViewSet):
serializer_class = TeamSerializer
model = Team
permission_classes = [WorkSpaceAdminPermission]
search_fields = ["member__display_name", "member__first_name"]
def get_queryset(self):
return self.filter_queryset(
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.select_related("workspace", "workspace__owner")
.prefetch_related("members")
)
def create(self, request, slug):
members = list(
WorkspaceMember.objects.filter(
workspace__slug=slug,
member__id__in=request.data.get("members", []),
is_active=True,
)
.annotate(member_str_id=Cast("member", output_field=CharField()))
.distinct()
.values_list("member_str_id", flat=True)
)
if len(members) != len(request.data.get("members", [])):
users = list(set(request.data.get("members", [])).difference(members))
users = User.objects.filter(pk__in=users)
serializer = UserLiteSerializer(users, many=True)
return Response(
{
"error": f"{len(users)} of the member(s) are not a part of the workspace",
"members": serializer.data,
},
status=status.HTTP_400_BAD_REQUEST,
)
workspace = Workspace.objects.get(slug=slug)
serializer = TeamSerializer(data=request.data, context={"workspace": workspace})
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

View file

@ -0,0 +1,242 @@
# Generated by Django 4.2.15 on 2024-11-27 09:07
from django.conf import settings
import django.contrib.postgres.fields
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import plane.db.models.webhook
import uuid
class Migration(migrations.Migration):
dependencies = [
("db", "0085_intake_intakeissue_remove_inboxissue_created_by_and_more"),
]
operations = [
migrations.CreateModel(
name="IssueVersion",
fields=[
(
"created_at",
models.DateTimeField(auto_now_add=True, verbose_name="Created At"),
),
(
"updated_at",
models.DateTimeField(
auto_now=True, verbose_name="Last Modified At"
),
),
(
"deleted_at",
models.DateTimeField(
blank=True, null=True, verbose_name="Deleted At"
),
),
(
"id",
models.UUIDField(
db_index=True,
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
unique=True,
),
),
("parent", models.UUIDField(blank=True, null=True)),
("state", models.UUIDField(blank=True, null=True)),
("estimate_point", models.UUIDField(blank=True, null=True)),
("name", models.CharField(max_length=255, verbose_name="Issue Name")),
("description", models.JSONField(blank=True, default=dict)),
("description_html", models.TextField(blank=True, default="<p></p>")),
("description_stripped", models.TextField(blank=True, null=True)),
("description_binary", models.BinaryField(null=True)),
(
"priority",
models.CharField(
choices=[
("urgent", "Urgent"),
("high", "High"),
("medium", "Medium"),
("low", "Low"),
("none", "None"),
],
default="none",
max_length=30,
verbose_name="Issue Priority",
),
),
("start_date", models.DateField(blank=True, null=True)),
("target_date", models.DateField(blank=True, null=True)),
(
"sequence_id",
models.IntegerField(default=1, verbose_name="Issue Sequence ID"),
),
("sort_order", models.FloatField(default=65535)),
("completed_at", models.DateTimeField(null=True)),
("archived_at", models.DateField(null=True)),
("is_draft", models.BooleanField(default=False)),
(
"external_source",
models.CharField(blank=True, max_length=255, null=True),
),
(
"external_id",
models.CharField(blank=True, max_length=255, null=True),
),
("type", models.UUIDField(blank=True, null=True)),
(
"last_saved_at",
models.DateTimeField(default=django.utils.timezone.now),
),
("owned_by", models.UUIDField()),
(
"assignees",
django.contrib.postgres.fields.ArrayField(
base_field=models.UUIDField(),
blank=True,
default=list,
size=None,
),
),
(
"labels",
django.contrib.postgres.fields.ArrayField(
base_field=models.UUIDField(),
blank=True,
default=list,
size=None,
),
),
("cycle", models.UUIDField(blank=True, null=True)),
(
"modules",
django.contrib.postgres.fields.ArrayField(
base_field=models.UUIDField(),
blank=True,
default=list,
size=None,
),
),
("properties", models.JSONField(default=dict)),
("meta", models.JSONField(default=dict)),
(
"created_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_created_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Created By",
),
),
(
"issue",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="versions",
to="db.issue",
),
),
(
"project",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="project_%(class)s",
to="db.project",
),
),
(
"updated_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_updated_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Last Modified By",
),
),
(
"workspace",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="workspace_%(class)s",
to="db.workspace",
),
),
],
options={
"verbose_name": "Issue Version",
"verbose_name_plural": "Issue Versions",
"db_table": "issue_versions",
"ordering": ("-created_at",),
},
),
migrations.AlterUniqueTogether(
name="teampage",
unique_together=None,
),
migrations.RemoveField(
model_name="teampage",
name="created_by",
),
migrations.RemoveField(
model_name="teampage",
name="page",
),
migrations.RemoveField(
model_name="teampage",
name="team",
),
migrations.RemoveField(
model_name="teampage",
name="updated_by",
),
migrations.RemoveField(
model_name="teampage",
name="workspace",
),
migrations.RemoveField(
model_name="page",
name="teams",
),
migrations.RemoveField(
model_name="team",
name="members",
),
migrations.AddField(
model_name="fileasset",
name="entity_identifier",
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name="webhook",
name="is_internal",
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name="fileasset",
name="entity_type",
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name="webhook",
name="url",
field=models.URLField(
max_length=1024,
validators=[
plane.db.models.webhook.validate_schema,
plane.db.models.webhook.validate_domain,
],
),
),
migrations.DeleteModel(
name="TeamMember",
),
migrations.DeleteModel(
name="TeamPage",
),
]

View file

@ -61,8 +61,6 @@ from .user import Account, Profile, User
from .view import IssueView from .view import IssueView
from .webhook import Webhook, WebhookLog from .webhook import Webhook, WebhookLog
from .workspace import ( from .workspace import (
Team,
TeamMember,
Workspace, Workspace,
WorkspaceBaseModel, WorkspaceBaseModel,
WorkspaceMember, WorkspaceMember,

View file

@ -44,25 +44,44 @@ class FileAsset(BaseModel):
"db.User", on_delete=models.CASCADE, null=True, related_name="assets" "db.User", on_delete=models.CASCADE, null=True, related_name="assets"
) )
workspace = models.ForeignKey( workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, null=True, related_name="assets" "db.Workspace",
on_delete=models.CASCADE,
null=True,
related_name="assets",
) )
draft_issue = models.ForeignKey( draft_issue = models.ForeignKey(
"db.DraftIssue", on_delete=models.CASCADE, null=True, related_name="assets" "db.DraftIssue",
on_delete=models.CASCADE,
null=True,
related_name="assets",
) )
project = models.ForeignKey( project = models.ForeignKey(
"db.Project", on_delete=models.CASCADE, null=True, related_name="assets" "db.Project",
on_delete=models.CASCADE,
null=True,
related_name="assets",
) )
issue = models.ForeignKey( issue = models.ForeignKey(
"db.Issue", on_delete=models.CASCADE, null=True, related_name="assets" "db.Issue", on_delete=models.CASCADE, null=True, related_name="assets"
) )
comment = models.ForeignKey( comment = models.ForeignKey(
"db.IssueComment", on_delete=models.CASCADE, null=True, related_name="assets" "db.IssueComment",
on_delete=models.CASCADE,
null=True,
related_name="assets",
) )
page = models.ForeignKey( page = models.ForeignKey(
"db.Page", on_delete=models.CASCADE, null=True, related_name="assets" "db.Page", on_delete=models.CASCADE, null=True, related_name="assets"
) )
entity_type = models.CharField( entity_type = models.CharField(
max_length=255, choices=EntityTypeContext.choices, null=True, blank=True max_length=255,
null=True,
blank=True,
)
entity_identifier = models.CharField(
max_length=255,
null=True,
blank=True,
) )
is_deleted = models.BooleanField(default=False) is_deleted = models.BooleanField(default=False)
is_archived = models.BooleanField(default=False) is_archived = models.BooleanField(default=False)

View file

@ -9,11 +9,12 @@ from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models, transaction from django.db import models, transaction
from django.utils import timezone from django.utils import timezone
from django.db.models import Q from django.db.models import Q
from django import apps
# Module imports # Module imports
from plane.utils.html_processor import strip_tags from plane.utils.html_processor import strip_tags
from plane.db.mixins import SoftDeletionManager from plane.db.mixins import SoftDeletionManager
from plane.utils.exception_logger import log_exception
from .project import ProjectBaseModel from .project import ProjectBaseModel
@ -656,3 +657,126 @@ class IssueVote(ProjectBaseModel):
def __str__(self): def __str__(self):
return f"{self.issue.name} {self.actor.email}" return f"{self.issue.name} {self.actor.email}"
class IssueVersion(ProjectBaseModel):
issue = models.ForeignKey(
"db.Issue",
on_delete=models.CASCADE,
related_name="versions",
)
PRIORITY_CHOICES = (
("urgent", "Urgent"),
("high", "High"),
("medium", "Medium"),
("low", "Low"),
("none", "None"),
)
parent = models.UUIDField(blank=True, null=True)
state = models.UUIDField(blank=True, null=True)
estimate_point = models.UUIDField(blank=True, null=True)
name = models.CharField(max_length=255, verbose_name="Issue Name")
description = models.JSONField(blank=True, default=dict)
description_html = models.TextField(blank=True, default="<p></p>")
description_stripped = models.TextField(blank=True, null=True)
description_binary = models.BinaryField(null=True)
priority = models.CharField(
max_length=30,
choices=PRIORITY_CHOICES,
verbose_name="Issue Priority",
default="none",
)
start_date = models.DateField(null=True, blank=True)
target_date = models.DateField(null=True, blank=True)
sequence_id = models.IntegerField(
default=1, verbose_name="Issue Sequence ID"
)
sort_order = models.FloatField(default=65535)
completed_at = models.DateTimeField(null=True)
archived_at = models.DateField(null=True)
is_draft = models.BooleanField(default=False)
external_source = models.CharField(max_length=255, null=True, blank=True)
external_id = models.CharField(max_length=255, blank=True, null=True)
type = models.UUIDField(blank=True, null=True)
last_saved_at = models.DateTimeField(default=timezone.now)
owned_by = models.UUIDField()
assignees = ArrayField(
models.UUIDField(),
blank=True,
default=list,
)
labels = ArrayField(
models.UUIDField(),
blank=True,
default=list,
)
cycle = models.UUIDField(
null=True,
blank=True,
)
modules = ArrayField(
models.UUIDField(),
blank=True,
default=list,
)
properties = models.JSONField(default=dict)
meta = models.JSONField(default=dict)
class Meta:
verbose_name = "Issue Version"
verbose_name_plural = "Issue Versions"
db_table = "issue_versions"
ordering = ("-created_at",)
def __str__(self):
return f"{self.name} <{self.project.name}>"
@classmethod
def log_issue_version(cls, issue, user):
try:
"""
Log the issue version
"""
Module = apps.get_model("db.Module")
CycleIssue = apps.get_model("db.CycleIssue")
cycle_issue = CycleIssue.objects.filter(
issue=issue,
).first()
cls.objects.create(
issue=issue,
parent=issue.parent,
state=issue.state,
point=issue.point,
estimate_point=issue.estimate_point,
name=issue.name,
description=issue.description,
description_html=issue.description_html,
description_stripped=issue.description_stripped,
description_binary=issue.description_binary,
priority=issue.priority,
start_date=issue.start_date,
target_date=issue.target_date,
sequence_id=issue.sequence_id,
sort_order=issue.sort_order,
completed_at=issue.completed_at,
archived_at=issue.archived_at,
is_draft=issue.is_draft,
external_source=issue.external_source,
external_id=issue.external_id,
type=issue.type,
last_saved_at=issue.last_saved_at,
assignees=issue.assignees,
labels=issue.labels,
cycle=cycle_issue.cycle if cycle_issue else None,
modules=Module.objects.filter(issue=issue).values_list(
"id", flat=True
),
owned_by=user,
)
return True
except Exception as e:
log_exception(e)
return False

View file

@ -50,9 +50,6 @@ class Page(BaseModel):
projects = models.ManyToManyField( projects = models.ManyToManyField(
"db.Project", related_name="pages", through="db.ProjectPage" "db.Project", related_name="pages", through="db.ProjectPage"
) )
teams = models.ManyToManyField(
"db.Team", related_name="pages", through="db.TeamPage"
)
class Meta: class Meta:
verbose_name = "Page" verbose_name = "Page"
@ -160,32 +157,6 @@ class ProjectPage(BaseModel):
return f"{self.project.name} {self.page.name}" return f"{self.project.name} {self.page.name}"
class TeamPage(BaseModel):
team = models.ForeignKey(
"db.Team", on_delete=models.CASCADE, related_name="team_pages"
)
page = models.ForeignKey(
"db.Page", on_delete=models.CASCADE, related_name="team_pages"
)
workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="team_pages"
)
class Meta:
unique_together = ["team", "page", "deleted_at"]
constraints = [
models.UniqueConstraint(
fields=["team", "page"],
condition=models.Q(deleted_at__isnull=True),
name="team_page_unique_team_page_when_deleted_at_null",
)
]
verbose_name = "Team Page"
verbose_name_plural = "Team Pages"
db_table = "team_pages"
ordering = ("-created_at",)
class PageVersion(BaseModel): class PageVersion(BaseModel):
workspace = models.ForeignKey( workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="page_versions" "db.Workspace", on_delete=models.CASCADE, related_name="page_versions"

View file

@ -29,9 +29,13 @@ def validate_domain(value):
class Webhook(BaseModel): class Webhook(BaseModel):
workspace = models.ForeignKey( workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="workspace_webhooks" "db.Workspace",
on_delete=models.CASCADE,
related_name="workspace_webhooks",
)
url = models.URLField(
validators=[validate_schema, validate_domain], max_length=1024
) )
url = models.URLField(validators=[validate_schema, validate_domain])
is_active = models.BooleanField(default=True) is_active = models.BooleanField(default=True)
secret_key = models.CharField(max_length=255, default=generate_token) secret_key = models.CharField(max_length=255, default=generate_token)
project = models.BooleanField(default=False) project = models.BooleanField(default=False)
@ -39,6 +43,7 @@ class Webhook(BaseModel):
module = models.BooleanField(default=False) module = models.BooleanField(default=False)
cycle = models.BooleanField(default=False) cycle = models.BooleanField(default=False)
issue_comment = models.BooleanField(default=False) issue_comment = models.BooleanField(default=False)
is_internal = models.BooleanField(default=False)
def __str__(self): def __str__(self):
return f"{self.workspace.slug} {self.url}" return f"{self.workspace.slug} {self.url}"

View file

@ -102,7 +102,12 @@ def get_default_display_properties():
def get_issue_props(): def get_issue_props():
return {"subscribed": True, "assigned": True, "created": True, "all_issues": True} return {
"subscribed": True,
"assigned": True,
"created": True,
"all_issues": True,
}
def slug_validator(value): def slug_validator(value):
@ -131,7 +136,9 @@ class Workspace(BaseModel):
max_length=48, db_index=True, unique=True, validators=[slug_validator] max_length=48, db_index=True, unique=True, validators=[slug_validator]
) )
organization_size = models.CharField(max_length=20, blank=True, null=True) organization_size = models.CharField(max_length=20, blank=True, null=True)
timezone = models.CharField(max_length=255, default="UTC", choices=TIMEZONE_CHOICES) timezone = models.CharField(
max_length=255, default="UTC", choices=TIMEZONE_CHOICES
)
def __str__(self): def __str__(self):
"""Return name of the Workspace""" """Return name of the Workspace"""
@ -160,7 +167,10 @@ class WorkspaceBaseModel(BaseModel):
"db.Workspace", models.CASCADE, related_name="workspace_%(class)s" "db.Workspace", models.CASCADE, related_name="workspace_%(class)s"
) )
project = models.ForeignKey( project = models.ForeignKey(
"db.Project", models.CASCADE, related_name="project_%(class)s", null=True "db.Project",
models.CASCADE,
related_name="project_%(class)s",
null=True,
) )
class Meta: class Meta:
@ -174,7 +184,9 @@ class WorkspaceBaseModel(BaseModel):
class WorkspaceMember(BaseModel): class WorkspaceMember(BaseModel):
workspace = models.ForeignKey( workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="workspace_member" "db.Workspace",
on_delete=models.CASCADE,
related_name="workspace_member",
) )
member = models.ForeignKey( member = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
@ -209,7 +221,9 @@ class WorkspaceMember(BaseModel):
class WorkspaceMemberInvite(BaseModel): class WorkspaceMemberInvite(BaseModel):
workspace = models.ForeignKey( workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="workspace_member_invite" "db.Workspace",
on_delete=models.CASCADE,
related_name="workspace_member_invite",
) )
email = models.CharField(max_length=255) email = models.CharField(max_length=255)
accepted = models.BooleanField(default=False) accepted = models.BooleanField(default=False)
@ -239,13 +253,6 @@ class WorkspaceMemberInvite(BaseModel):
class Team(BaseModel): class Team(BaseModel):
name = models.CharField(max_length=255, verbose_name="Team Name") name = models.CharField(max_length=255, verbose_name="Team Name")
description = models.TextField(verbose_name="Team Description", blank=True) description = models.TextField(verbose_name="Team Description", blank=True)
members = models.ManyToManyField(
settings.AUTH_USER_MODEL,
blank=True,
related_name="members",
through="TeamMember",
through_fields=("team", "member"),
)
workspace = models.ForeignKey( workspace = models.ForeignKey(
Workspace, on_delete=models.CASCADE, related_name="workspace_team" Workspace, on_delete=models.CASCADE, related_name="workspace_team"
) )
@ -270,40 +277,15 @@ class Team(BaseModel):
ordering = ("-created_at",) ordering = ("-created_at",)
class TeamMember(BaseModel):
workspace = models.ForeignKey(
Workspace, on_delete=models.CASCADE, related_name="team_member"
)
team = models.ForeignKey(Team, on_delete=models.CASCADE, related_name="team_member")
member = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="team_member"
)
def __str__(self):
return self.team.name
class Meta:
unique_together = ["team", "member", "deleted_at"]
constraints = [
models.UniqueConstraint(
fields=["team", "member"],
condition=models.Q(deleted_at__isnull=True),
name="team_member_unique_team_member_when_deleted_at_null",
)
]
verbose_name = "Team Member"
verbose_name_plural = "Team Members"
db_table = "team_members"
ordering = ("-created_at",)
class WorkspaceTheme(BaseModel): class WorkspaceTheme(BaseModel):
workspace = models.ForeignKey( workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="themes" "db.Workspace", on_delete=models.CASCADE, related_name="themes"
) )
name = models.CharField(max_length=300) name = models.CharField(max_length=300)
actor = models.ForeignKey( actor = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="themes" settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="themes",
) )
colors = models.JSONField(default=dict) colors = models.JSONField(default=dict)
@ -338,7 +320,9 @@ class WorkspaceUserProperties(BaseModel):
) )
filters = models.JSONField(default=get_default_filters) filters = models.JSONField(default=get_default_filters)
display_filters = models.JSONField(default=get_default_display_filters) display_filters = models.JSONField(default=get_default_display_filters)
display_properties = models.JSONField(default=get_default_display_properties) display_properties = models.JSONField(
default=get_default_display_properties
)
class Meta: class Meta:
unique_together = ["workspace", "user", "deleted_at"] unique_together = ["workspace", "user", "deleted_at"]