chore: user issue display properties (#2258)

* chore: user issue display properties

* chore: added issue property

* fix: migrations and url change

* dev: add a default condition on get for issue properties

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
Nikhil 2023-10-27 15:32:42 +05:30 committed by GitHub
parent c8f98a9bc2
commit 6bebb8a93b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 154 additions and 89 deletions

View File

@ -17,7 +17,7 @@ from plane.api.views import (
IssueSubscriberViewSet, IssueSubscriberViewSet,
IssueReactionViewSet, IssueReactionViewSet,
CommentReactionViewSet, CommentReactionViewSet,
IssuePropertyViewSet, IssueUserDisplayPropertyEndpoint,
IssueArchiveViewSet, IssueArchiveViewSet,
IssueRelationViewSet, IssueRelationViewSet,
IssueDraftViewSet, IssueDraftViewSet,
@ -235,28 +235,11 @@ urlpatterns = [
## End Comment Reactions ## End Comment Reactions
## IssueProperty ## IssueProperty
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-properties/", "workspaces/<str:slug>/projects/<uuid:project_id>/issue-display-properties/",
IssuePropertyViewSet.as_view( IssueUserDisplayPropertyEndpoint.as_view(),
{ name="project-issue-display-properties",
"get": "list",
"post": "create",
}
), ),
name="project-issue-roadmap", ## IssueProperty End
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-properties/<uuid:pk>/",
IssuePropertyViewSet.as_view(
{
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-issue-roadmap",
),
## IssueProperty Ebd
## Issue Archives ## Issue Archives
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/", "workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/",

View File

@ -82,7 +82,7 @@ from plane.api.views import (
BulkDeleteIssuesEndpoint, BulkDeleteIssuesEndpoint,
BulkImportIssuesEndpoint, BulkImportIssuesEndpoint,
ProjectUserViewsEndpoint, ProjectUserViewsEndpoint,
IssuePropertyViewSet, IssueUserDisplayPropertyEndpoint,
LabelViewSet, LabelViewSet,
SubIssuesEndpoint, SubIssuesEndpoint,
IssueLinkViewSet, IssueLinkViewSet,
@ -1008,26 +1008,9 @@ urlpatterns = [
## End Comment Reactions ## End Comment Reactions
## IssueProperty ## IssueProperty
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-properties/", "workspaces/<str:slug>/projects/<uuid:project_id>/issue-display-properties/",
IssuePropertyViewSet.as_view( IssueUserDisplayPropertyEndpoint.as_view(),
{ name="project-issue-display-properties",
"get": "list",
"post": "create",
}
),
name="project-issue-roadmap",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-properties/<uuid:pk>/",
IssuePropertyViewSet.as_view(
{
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-issue-roadmap",
), ),
## IssueProperty Ebd ## IssueProperty Ebd
## Issue Archives ## Issue Archives

View File

@ -71,7 +71,7 @@ from .issue import (
WorkSpaceIssuesEndpoint, WorkSpaceIssuesEndpoint,
IssueActivityEndpoint, IssueActivityEndpoint,
IssueCommentViewSet, IssueCommentViewSet,
IssuePropertyViewSet, IssueUserDisplayPropertyEndpoint,
LabelViewSet, LabelViewSet,
BulkDeleteIssuesEndpoint, BulkDeleteIssuesEndpoint,
UserWorkSpaceIssues, UserWorkSpaceIssues,

View File

@ -606,41 +606,12 @@ class IssueCommentViewSet(BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
class IssuePropertyViewSet(BaseViewSet): class IssueUserDisplayPropertyEndpoint(BaseAPIView):
serializer_class = IssuePropertySerializer
model = IssueProperty
permission_classes = [ permission_classes = [
ProjectEntityPermission, ProjectLitePermission,
] ]
filterset_fields = [] def post(self, request, slug, project_id):
def perform_create(self, serializer):
serializer.save(
project_id=self.kwargs.get("project_id"), user=self.request.user
)
def get_queryset(self):
return self.filter_queryset(
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(user=self.request.user)
.filter(project__project_projectmember__member=self.request.user)
.select_related("project")
.select_related("workspace")
)
def list(self, request, slug, project_id):
queryset = self.get_queryset()
serializer = IssuePropertySerializer(queryset, many=True)
return Response(
serializer.data[0] if len(serializer.data) > 0 else [],
status=status.HTTP_200_OK,
)
def create(self, request, slug, project_id):
issue_property, created = IssueProperty.objects.get_or_create( issue_property, created = IssueProperty.objects.get_or_create(
user=request.user, user=request.user,
project_id=project_id, project_id=project_id,
@ -649,16 +620,20 @@ class IssuePropertyViewSet(BaseViewSet):
if not created: if not created:
issue_property.properties = request.data.get("properties", {}) issue_property.properties = request.data.get("properties", {})
issue_property.save() issue_property.save()
serializer = IssuePropertySerializer(issue_property)
return Response(serializer.data, status=status.HTTP_200_OK)
issue_property.properties = request.data.get("properties", {}) issue_property.properties = request.data.get("properties", {})
issue_property.save() issue_property.save()
serializer = IssuePropertySerializer(issue_property) serializer = IssuePropertySerializer(issue_property)
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
def get(self, request, slug, project_id):
issue_property, _ = IssueProperty.objects.get_or_create(
user=request.user, project_id=project_id
)
serializer = IssuePropertySerializer(issue_property)
return Response(serializer.data, status=status.HTTP_200_OK)
class LabelViewSet(BaseViewSet): class LabelViewSet(BaseViewSet):
serializer_class = LabelSerializer serializer_class = LabelSerializer
model = Label model = Label
@ -972,8 +947,8 @@ class IssueAttachmentEndpoint(BaseAPIView):
issue_attachments = IssueAttachment.objects.filter( issue_attachments = IssueAttachment.objects.filter(
issue_id=issue_id, workspace__slug=slug, project_id=project_id issue_id=issue_id, workspace__slug=slug, project_id=project_id
) )
serilaizer = IssueAttachmentSerializer(issue_attachments, many=True) serializer = IssueAttachmentSerializer(issue_attachments, many=True)
return Response(serilaizer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
class IssueArchiveViewSet(BaseViewSet): class IssueArchiveViewSet(BaseViewSet):

View File

@ -69,6 +69,7 @@ from plane.db.models import (
ModuleMember, ModuleMember,
Inbox, Inbox,
ProjectDeployBoard, ProjectDeployBoard,
IssueProperty,
) )
from plane.bgtasks.project_invitation_task import project_invitation from plane.bgtasks.project_invitation_task import project_invitation
@ -201,6 +202,11 @@ class ProjectViewSet(BaseViewSet):
project_member = ProjectMember.objects.create( project_member = ProjectMember.objects.create(
project_id=serializer.data["id"], member=request.user, role=20 project_id=serializer.data["id"], member=request.user, role=20
) )
# Also create the issue property for the user
_ = IssueProperty.objects.create(
project_id=serializer.data["id"],
user=request.user,
)
if serializer.data["project_lead"] is not None and str( if serializer.data["project_lead"] is not None and str(
serializer.data["project_lead"] serializer.data["project_lead"]
@ -210,6 +216,11 @@ class ProjectViewSet(BaseViewSet):
member_id=serializer.data["project_lead"], member_id=serializer.data["project_lead"],
role=20, role=20,
) )
# Also create the issue property for the user
IssueProperty.objects.create(
project_id=serializer.data["id"],
user_id=serializer.data["project_lead"],
)
# Default states # Default states
states = [ states = [
@ -393,6 +404,8 @@ class InviteProjectEndpoint(BaseAPIView):
member=user, project_id=project_id, role=role member=user, project_id=project_id, role=role
) )
_ = IssueProperty.objects.create(user=user, project_id=project_id)
return Response( return Response(
ProjectMemberSerializer(project_member).data, status=status.HTTP_200_OK ProjectMemberSerializer(project_member).data, status=status.HTTP_200_OK
) )
@ -428,6 +441,18 @@ class UserProjectInvitationsViewset(BaseViewSet):
] ]
) )
IssueProperty.objects.bulk_create(
[
ProjectMember(
project=invitation.project,
workspace=invitation.project.workspace,
user=request.user,
created_by=request.user,
)
for invitation in project_invitations
]
)
# Delete joined project invites # Delete joined project invites
project_invitations.delete() project_invitations.delete()
@ -560,6 +585,7 @@ class AddMemberToProjectEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
bulk_project_members = [] bulk_project_members = []
bulk_issue_props = []
project_members = ( project_members = (
ProjectMember.objects.filter( ProjectMember.objects.filter(
@ -574,7 +600,8 @@ class AddMemberToProjectEndpoint(BaseAPIView):
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"))
] ]
bulk_project_members.append( bulk_project_members.append(
ProjectMember( ProjectMember(
@ -585,6 +612,13 @@ class AddMemberToProjectEndpoint(BaseAPIView):
sort_order=sort_order[0] - 10000 if len(sort_order) else 65535, sort_order=sort_order[0] - 10000 if len(sort_order) else 65535,
) )
) )
bulk_issue_props.append(
IssueProperty(
user_id=member.get("member_id"),
project_id=project_id,
workspace_id=project.workspace_id,
)
)
project_members = ProjectMember.objects.bulk_create( project_members = ProjectMember.objects.bulk_create(
bulk_project_members, bulk_project_members,
@ -592,7 +626,12 @@ class AddMemberToProjectEndpoint(BaseAPIView):
ignore_conflicts=True, ignore_conflicts=True,
) )
_ = IssueProperty.objects.bulk_create(
bulk_issue_props, batch_size=10, ignore_conflicts=True
)
serializer = ProjectMemberSerializer(project_members, many=True) serializer = ProjectMemberSerializer(project_members, many=True)
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
@ -614,6 +653,7 @@ class AddTeamToProjectEndpoint(BaseAPIView):
workspace = Workspace.objects.get(slug=slug) workspace = Workspace.objects.get(slug=slug)
project_members = [] project_members = []
issue_props = []
for member in team_members: for member in team_members:
project_members.append( project_members.append(
ProjectMember( ProjectMember(
@ -623,11 +663,23 @@ class AddTeamToProjectEndpoint(BaseAPIView):
created_by=request.user, created_by=request.user,
) )
) )
issue_props.append(
IssueProperty(
project_id=project_id,
user_id=member,
workspace=workspace,
created_by=request.user,
)
)
ProjectMember.objects.bulk_create( ProjectMember.objects.bulk_create(
project_members, batch_size=10, ignore_conflicts=True project_members, batch_size=10, ignore_conflicts=True
) )
_ = IssueProperty.objects.bulk_create(
issue_props, batch_size=10, ignore_conflicts=True
)
serializer = ProjectMemberSerializer(project_members, many=True) serializer = ProjectMemberSerializer(project_members, many=True)
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
@ -743,6 +795,19 @@ class ProjectJoinEndpoint(BaseAPIView):
ignore_conflicts=True, ignore_conflicts=True,
) )
IssueProperty.objects.bulk_create(
[
IssueProperty(
project_id=project_id,
user=request.user,
workspace=workspace,
created_by=request.user,
)
for project_id in project_ids
],
ignore_conflicts=True,
)
return Response( return Response(
{"message": "Projects joined successfully"}, {"message": "Projects joined successfully"},
status=status.HTTP_201_CREATED, status=status.HTTP_201_CREATED,

View File

@ -25,6 +25,7 @@ from plane.db.models import (
WorkspaceIntegration, WorkspaceIntegration,
Label, Label,
User, User,
IssueProperty,
) )
from .workspace_invitation_task import workspace_invitation from .workspace_invitation_task import workspace_invitation
from plane.bgtasks.user_welcome_task import send_welcome_slack from plane.bgtasks.user_welcome_task import send_welcome_slack
@ -103,6 +104,20 @@ def service_importer(service, importer_id):
ignore_conflicts=True, ignore_conflicts=True,
) )
IssueProperty.objects.bulk_create(
[
IssueProperty(
project_id=importer.project_id,
workspace_id=importer.workspace_id,
user=user,
created_by=importer.created_by,
)
for user in workspace_users
],
batch_size=100,
ignore_conflicts=True,
)
# Check if sync config is on for github importers # Check if sync config is on for github importers
if service == "github" and importer.config.get("sync", False): if service == "github" and importer.config.get("sync", False):
name = importer.metadata.get("name", False) name = importer.metadata.get("name", False)

View File

@ -0,0 +1,21 @@
# Generated by Django 4.2.5 on 2023-10-18 12:04
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import plane.db.models.issue
class Migration(migrations.Migration):
dependencies = [
('db', '0045_issueactivity_epoch_workspacemember_issue_props_and_more'),
]
operations = [
migrations.AlterField(
model_name='issueproperty',
name='properties',
field=models.JSONField(default=plane.db.models.issue.get_default_properties),
),
]

View File

@ -16,6 +16,24 @@ from . import ProjectBaseModel
from plane.utils.html_processor import strip_tags from plane.utils.html_processor import strip_tags
def get_default_properties():
return {
"assignee": True,
"start_date": True,
"due_date": True,
"labels": True,
"key": True,
"priority": True,
"state": True,
"sub_issue_count": True,
"link": True,
"attachment_count": True,
"estimate": True,
"created_on": True,
"updated_on": True,
}
# TODO: Handle identifiers for Bulk Inserts - nk # TODO: Handle identifiers for Bulk Inserts - nk
class IssueManager(models.Manager): class IssueManager(models.Manager):
def get_queryset(self): def get_queryset(self):
@ -39,7 +57,7 @@ class Issue(ProjectBaseModel):
("high", "High"), ("high", "High"),
("medium", "Medium"), ("medium", "Medium"),
("low", "Low"), ("low", "Low"),
("none", "None") ("none", "None"),
) )
parent = models.ForeignKey( parent = models.ForeignKey(
"self", "self",
@ -327,7 +345,9 @@ class IssueComment(ProjectBaseModel):
comment_json = models.JSONField(blank=True, default=dict) comment_json = models.JSONField(blank=True, default=dict)
comment_html = models.TextField(blank=True, default="<p></p>") comment_html = models.TextField(blank=True, default="<p></p>")
attachments = ArrayField(models.URLField(), size=10, blank=True, default=list) attachments = ArrayField(models.URLField(), size=10, blank=True, default=list)
issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="issue_comments") issue = models.ForeignKey(
Issue, on_delete=models.CASCADE, related_name="issue_comments"
)
# System can also create comment # System can also create comment
actor = models.ForeignKey( actor = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
@ -367,7 +387,7 @@ class IssueProperty(ProjectBaseModel):
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name="issue_property_user", related_name="issue_property_user",
) )
properties = models.JSONField(default=dict) properties = models.JSONField(default=get_default_properties)
class Meta: class Meta:
verbose_name = "Issue Property" verbose_name = "Issue Property"
@ -515,7 +535,10 @@ class IssueVote(ProjectBaseModel):
) )
class Meta: class Meta:
unique_together = ["issue", "actor",] unique_together = [
"issue",
"actor",
]
verbose_name = "Issue Vote" verbose_name = "Issue Vote"
verbose_name_plural = "Issue Votes" verbose_name_plural = "Issue Votes"
db_table = "issue_votes" db_table = "issue_votes"