feat: user display name (#1179)

* feat: user display name for the entire system

* feat: update issue activity to remove emails

* dev: update to display name wherever assignees__email and member__email

* dev: update display names on issue activity and the user script

* dev: update display_name function to generate display_name from email

* dev: add email for test purpose

* dev: set default display name for the user

* dev: add migration script and default value

* dev: annotate with assignees_id

* dev: return assignees id

* dev: display name for the profile

* dev: project members endpoint

* dev: url update

* dev: trailing /

* dev: update workspace member serializer

* fix: activity for assignees
This commit is contained in:
Nikhil 2023-08-08 12:59:04 +05:30 committed by GitHub
parent 9df0ba6e3a
commit cf306ee605
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 330 additions and 157 deletions

View File

@ -1,4 +1,4 @@
import os, sys import os, sys, random, string
import uuid import uuid
sys.path.append("/code") sys.path.append("/code")
@ -19,9 +19,9 @@ def populate():
user = User.objects.create(email=default_email, username=uuid.uuid4().hex) user = User.objects.create(email=default_email, username=uuid.uuid4().hex)
user.set_password(default_password) user.set_password(default_password)
user.save() user.save()
print("User created") print(f"User created with an email: {default_email}")
else:
print("Success") print(f"User already exists with the default email: {default_email}")
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -1,10 +1,5 @@
from .base import BaseSerializer from .base import BaseSerializer
from .people import ( from .user import UserSerializer, UserLiteSerializer, ChangePasswordSerializer, ResetPasswordSerializer, UserAdminLiteSerializer
ChangePasswordSerializer,
ResetPasswordSerializer,
TokenSerializer,
)
from .user import UserSerializer, UserLiteSerializer
from .workspace import ( from .workspace import (
WorkSpaceSerializer, WorkSpaceSerializer,
WorkSpaceMemberSerializer, WorkSpaceMemberSerializer,
@ -12,6 +7,7 @@ from .workspace import (
WorkSpaceMemberInviteSerializer, WorkSpaceMemberInviteSerializer,
WorkspaceLiteSerializer, WorkspaceLiteSerializer,
WorkspaceThemeSerializer, WorkspaceThemeSerializer,
WorkspaceMemberAdminSerializer,
) )
from .project import ( from .project import (
ProjectSerializer, ProjectSerializer,

View File

@ -1,57 +0,0 @@
from rest_framework.serializers import (
ModelSerializer,
Serializer,
CharField,
SerializerMethodField,
)
from rest_framework.authtoken.models import Token
from rest_framework_simplejwt.tokens import RefreshToken
from plane.db.models import User
class UserSerializer(ModelSerializer):
class Meta:
model = User
fields = "__all__"
extra_kwargs = {"password": {"write_only": True}}
class ChangePasswordSerializer(Serializer):
model = User
"""
Serializer for password change endpoint.
"""
old_password = CharField(required=True)
new_password = CharField(required=True)
class ResetPasswordSerializer(Serializer):
model = User
"""
Serializer for password change endpoint.
"""
new_password = CharField(required=True)
confirm_password = CharField(required=True)
class TokenSerializer(ModelSerializer):
user = UserSerializer()
access_token = SerializerMethodField()
refresh_token = SerializerMethodField()
def get_access_token(self, obj):
refresh_token = RefreshToken.for_user(obj.user)
return str(refresh_token.access_token)
def get_refresh_token(self, obj):
refresh_token = RefreshToken.for_user(obj.user)
return str(refresh_token)
class Meta:
model = Token
fields = "__all__"

View File

@ -7,7 +7,7 @@ from rest_framework import serializers
# Module imports # Module imports
from .base import BaseSerializer from .base import BaseSerializer
from plane.api.serializers.workspace import WorkSpaceSerializer, WorkspaceLiteSerializer from plane.api.serializers.workspace import WorkSpaceSerializer, WorkspaceLiteSerializer
from plane.api.serializers.user import UserLiteSerializer from plane.api.serializers.user import UserLiteSerializer, UserAdminLiteSerializer
from plane.db.models import ( from plane.db.models import (
Project, Project,
ProjectMember, ProjectMember,
@ -110,6 +110,17 @@ class ProjectMemberSerializer(BaseSerializer):
fields = "__all__" fields = "__all__"
class ProjectMemberAdminSerializer(BaseSerializer):
workspace = WorkspaceLiteSerializer(read_only=True)
project = ProjectLiteSerializer(read_only=True)
member = UserAdminLiteSerializer(read_only=True)
class Meta:
model = ProjectMember
fields = "__all__"
class ProjectMemberInviteSerializer(BaseSerializer): class ProjectMemberInviteSerializer(BaseSerializer):
project = ProjectLiteSerializer(read_only=True) project = ProjectLiteSerializer(read_only=True)
workspace = WorkspaceLiteSerializer(read_only=True) workspace = WorkspaceLiteSerializer(read_only=True)

View File

@ -1,3 +1,6 @@
# Third party imports
from rest_framework import serializers
# Module import # Module import
from .base import BaseSerializer from .base import BaseSerializer
from plane.db.models import User from plane.db.models import User
@ -37,11 +40,50 @@ class UserLiteSerializer(BaseSerializer):
"id", "id",
"first_name", "first_name",
"last_name", "last_name",
"email",
"avatar", "avatar",
"is_bot", "is_bot",
"display_name",
] ]
read_only_fields = [ read_only_fields = [
"id", "id",
"is_bot", "is_bot",
] ]
class UserAdminLiteSerializer(BaseSerializer):
class Meta:
model = User
fields = [
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
"email",
]
read_only_fields = [
"id",
"is_bot",
]
class ChangePasswordSerializer(serializers.Serializer):
model = User
"""
Serializer for password change endpoint.
"""
old_password = serializers.CharField(required=True)
new_password = serializers.CharField(required=True)
class ResetPasswordSerializer(serializers.Serializer):
model = User
"""
Serializer for password change endpoint.
"""
new_password = serializers.CharField(required=True)
confirm_password = serializers.CharField(required=True)

View File

@ -3,7 +3,7 @@ from rest_framework import serializers
# Module imports # Module imports
from .base import BaseSerializer from .base import BaseSerializer
from .user import UserLiteSerializer from .user import UserLiteSerializer, UserAdminLiteSerializer
from plane.db.models import ( from plane.db.models import (
User, User,
@ -33,10 +33,30 @@ class WorkSpaceSerializer(BaseSerializer):
"owner", "owner",
] ]
class WorkspaceLiteSerializer(BaseSerializer):
class Meta:
model = Workspace
fields = [
"name",
"slug",
"id",
]
read_only_fields = fields
class WorkSpaceMemberSerializer(BaseSerializer): class WorkSpaceMemberSerializer(BaseSerializer):
member = UserLiteSerializer(read_only=True) member = UserLiteSerializer(read_only=True)
workspace = WorkSpaceSerializer(read_only=True) workspace = WorkspaceLiteSerializer(read_only=True)
class Meta:
model = WorkspaceMember
fields = "__all__"
class WorkspaceMemberAdminSerializer(BaseSerializer):
member = UserAdminLiteSerializer(read_only=True)
workspace = WorkspaceLiteSerializer(read_only=True)
class Meta: class Meta:
model = WorkspaceMember model = WorkspaceMember
@ -101,17 +121,6 @@ class TeamSerializer(BaseSerializer):
return super().update(instance, validated_data) return super().update(instance, validated_data)
class WorkspaceLiteSerializer(BaseSerializer):
class Meta:
model = Workspace
fields = [
"name",
"slug",
"id",
]
read_only_fields = fields
class WorkspaceThemeSerializer(BaseSerializer): class WorkspaceThemeSerializer(BaseSerializer):
class Meta: class Meta:
model = WorkspaceTheme model = WorkspaceTheme

View File

@ -32,6 +32,7 @@ from plane.api.views import (
InviteWorkspaceEndpoint, InviteWorkspaceEndpoint,
JoinWorkspaceEndpoint, JoinWorkspaceEndpoint,
WorkSpaceMemberViewSet, WorkSpaceMemberViewSet,
WorkspaceMembersEndpoint,
WorkspaceInvitationsViewset, WorkspaceInvitationsViewset,
UserWorkspaceInvitationsEndpoint, UserWorkspaceInvitationsEndpoint,
WorkspaceMemberUserEndpoint, WorkspaceMemberUserEndpoint,
@ -59,6 +60,7 @@ from plane.api.views import (
ProjectViewSet, ProjectViewSet,
InviteProjectEndpoint, InviteProjectEndpoint,
ProjectMemberViewSet, ProjectMemberViewSet,
ProjectMemberEndpoint,
ProjectMemberInvitationsViewset, ProjectMemberInvitationsViewset,
ProjectMemberUserEndpoint, ProjectMemberUserEndpoint,
AddMemberToProjectEndpoint, AddMemberToProjectEndpoint,
@ -335,6 +337,11 @@ urlpatterns = [
), ),
name="workspace", name="workspace",
), ),
path(
"workspaces/<str:slug>/workspace-members/",
WorkspaceMembersEndpoint.as_view(),
name="workspace-members",
),
path( path(
"workspaces/<str:slug>/teams/", "workspaces/<str:slug>/teams/",
TeamMemberViewSet.as_view( TeamMemberViewSet.as_view(
@ -468,6 +475,11 @@ urlpatterns = [
), ),
name="project", name="project",
), ),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-members/",
ProjectMemberEndpoint.as_view(),
name="project",
),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/members/add/", "workspaces/<str:slug>/projects/<uuid:project_id>/members/add/",
AddMemberToProjectEndpoint.as_view(), AddMemberToProjectEndpoint.as_view(),

View File

@ -12,8 +12,9 @@ from .project import (
ProjectUserViewsEndpoint, ProjectUserViewsEndpoint,
ProjectMemberUserEndpoint, ProjectMemberUserEndpoint,
ProjectFavoritesViewSet, ProjectFavoritesViewSet,
ProjectMemberEndpoint,
) )
from .people import ( from .user import (
UserEndpoint, UserEndpoint,
UpdateUserOnBoardedEndpoint, UpdateUserOnBoardedEndpoint,
UpdateUserTourCompletedEndpoint, UpdateUserTourCompletedEndpoint,
@ -47,6 +48,7 @@ from .workspace import (
WorkspaceUserProfileEndpoint, WorkspaceUserProfileEndpoint,
WorkspaceUserProfileIssuesEndpoint, WorkspaceUserProfileIssuesEndpoint,
WorkspaceLabelsEndpoint, WorkspaceLabelsEndpoint,
WorkspaceMembersEndpoint,
) )
from .state import StateViewSet from .state import StateViewSet
from .view import IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet from .view import IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet

View File

@ -79,12 +79,12 @@ class AnalyticsEndpoint(BaseAPIView):
) )
assignee_details = {} assignee_details = {}
if x_axis in ["assignees__email"] or segment in ["assignees__email"]: if x_axis in ["assignees__id"] or segment in ["assignees__id"]:
assignee_details = ( assignee_details = (
Issue.issue_objects.filter(workspace__slug=slug, **filters, assignees__avatar__isnull=False) Issue.issue_objects.filter(workspace__slug=slug, **filters, assignees__avatar__isnull=False)
.order_by("assignees__id") .order_by("assignees__id")
.distinct("assignees__id") .distinct("assignees__id")
.values("assignees__avatar", "assignees__email", "assignees__first_name", "assignees__last_name") .values("assignees__avatar", "assignees__display_name", "assignees__first_name", "assignees__last_name", "assignees__id")
) )
@ -243,21 +243,21 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
) )
most_issue_created_user = ( most_issue_created_user = (
queryset.exclude(created_by=None) queryset.exclude(created_by=None)
.values("created_by__first_name", "created_by__last_name", "created_by__avatar", "created_by__email") .values("created_by__first_name", "created_by__last_name", "created_by__avatar", "created_by__display_name")
.annotate(count=Count("id")) .annotate(count=Count("id"))
.order_by("-count") .order_by("-count")
)[:5] )[:5]
most_issue_closed_user = ( most_issue_closed_user = (
queryset.filter(completed_at__isnull=False, assignees__isnull=False) queryset.filter(completed_at__isnull=False, assignees__isnull=False)
.values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__email") .values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__display_name")
.annotate(count=Count("id")) .annotate(count=Count("id"))
.order_by("-count") .order_by("-count")
)[:5] )[:5]
pending_issue_user = ( pending_issue_user = (
queryset.filter(completed_at__isnull=True) queryset.filter(completed_at__isnull=True)
.values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__email") .values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__display_name")
.annotate(count=Count("id")) .annotate(count=Count("id"))
.order_by("-count") .order_by("-count")
) )

View File

@ -22,7 +22,7 @@ from sentry_sdk import capture_exception
## Module imports ## Module imports
from . import BaseAPIView from . import BaseAPIView
from plane.api.serializers.people import ( from plane.api.serializers import (
ChangePasswordSerializer, ChangePasswordSerializer,
ResetPasswordSerializer, ResetPasswordSerializer,
) )

View File

@ -458,7 +458,7 @@ class BulkImportIssuesEndpoint(BaseAPIView):
actor=request.user, actor=request.user,
project_id=project_id, project_id=project_id,
workspace_id=project.workspace_id, workspace_id=project.workspace_id,
comment=f"{request.user.email} importer the issue from {service}", comment=f"imported the issue from {service}",
verb="created", verb="created",
created_by=request.user, created_by=request.user,
) )

View File

@ -301,7 +301,7 @@ class CreateIssueFromPageBlockEndpoint(BaseAPIView):
issue=issue, issue=issue,
actor=request.user, actor=request.user,
project_id=project_id, project_id=project_id,
comment=f"{request.user.email} created the issue from {page_block.name} block", comment=f"created the issue from {page_block.name} block",
verb="created", verb="created",
) )

View File

@ -25,7 +25,7 @@ from plane.api.serializers import (
ProjectFavoriteSerializer, ProjectFavoriteSerializer,
) )
from plane.api.permissions import ProjectBasePermission from plane.api.permissions import ProjectBasePermission, ProjectEntityPermission
from plane.db.models import ( from plane.db.models import (
Project, Project,
@ -458,7 +458,7 @@ class ProjectMemberViewSet(BaseViewSet):
] ]
search_fields = [ search_fields = [
"member__email", "member__display_name",
"member__first_name", "member__first_name",
] ]
@ -984,3 +984,23 @@ class ProjectFavoritesViewSet(BaseViewSet):
{"error": "Something went wrong please try again later"}, {"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
class ProjectMemberEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def get(self, request, slug, project_id):
try:
project_members = ProjectMember.objects.filter(
project_id=project_id, workspace__slug=slug
).select_related("project", "member")
serializer = ProjectMemberSerializer(project_members, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)

View File

@ -47,6 +47,7 @@ from plane.api.serializers import (
WorkspaceThemeSerializer, WorkspaceThemeSerializer,
IssueActivitySerializer, IssueActivitySerializer,
IssueLiteSerializer, IssueLiteSerializer,
WorkspaceMemberAdminSerializer
) )
from plane.api.views.base import BaseAPIView from plane.api.views.base import BaseAPIView
from . import BaseViewSet from . import BaseViewSet
@ -537,7 +538,7 @@ class UserWorkspaceInvitationsEndpoint(BaseViewSet):
class WorkSpaceMemberViewSet(BaseViewSet): class WorkSpaceMemberViewSet(BaseViewSet):
serializer_class = WorkSpaceMemberSerializer serializer_class = WorkspaceMemberAdminSerializer
model = WorkspaceMember model = WorkspaceMember
permission_classes = [ permission_classes = [
@ -545,7 +546,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
] ]
search_fields = [ search_fields = [
"member__email", "member__display_name",
"member__first_name", "member__first_name",
] ]
@ -690,7 +691,7 @@ class TeamMemberViewSet(BaseViewSet):
] ]
search_fields = [ search_fields = [
"member__email", "member__display_name",
"member__first_name", "member__first_name",
] ]
@ -1048,7 +1049,6 @@ class WorkspaceThemeViewSet(BaseViewSet):
class WorkspaceUserProfileStatsEndpoint(BaseAPIView): class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
def get(self, request, slug, user_id): def get(self, request, slug, user_id):
try: try:
filters = issue_filters(request.query_params, "GET") filters = issue_filters(request.query_params, "GET")
@ -1146,14 +1146,18 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
upcoming_cycles = CycleIssue.objects.filter( upcoming_cycles = CycleIssue.objects.filter(
workspace__slug=slug, workspace__slug=slug,
cycle__start_date__gt=timezone.now().date(), cycle__start_date__gt=timezone.now().date(),
issue__assignees__in=[user_id,] issue__assignees__in=[
user_id,
],
).values("cycle__name", "cycle__id", "cycle__project_id") ).values("cycle__name", "cycle__id", "cycle__project_id")
present_cycle = CycleIssue.objects.filter( present_cycle = CycleIssue.objects.filter(
workspace__slug=slug, workspace__slug=slug,
cycle__start_date__lt=timezone.now().date(), cycle__start_date__lt=timezone.now().date(),
cycle__end_date__gt=timezone.now().date(), cycle__end_date__gt=timezone.now().date(),
issue__assignees__in=[user_id,] issue__assignees__in=[
user_id,
],
).values("cycle__name", "cycle__id", "cycle__project_id") ).values("cycle__name", "cycle__id", "cycle__project_id")
return Response( return Response(
@ -1166,7 +1170,7 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
"pending_issues": pending_issues_count, "pending_issues": pending_issues_count,
"subscribed_issues": subscribed_issues_count, "subscribed_issues": subscribed_issues_count,
"present_cycles": present_cycle, "present_cycles": present_cycle,
"upcoming_cycles": upcoming_cycles, "upcoming_cycles": upcoming_cycles,
} }
) )
except Exception as e: except Exception as e:
@ -1184,7 +1188,6 @@ class WorkspaceUserActivityEndpoint(BaseAPIView):
def get(self, request, slug, user_id): def get(self, request, slug, user_id):
try: try:
projects = request.query_params.getlist("project", []) projects = request.query_params.getlist("project", [])
queryset = IssueActivity.objects.filter( queryset = IssueActivity.objects.filter(
@ -1212,12 +1215,13 @@ class WorkspaceUserActivityEndpoint(BaseAPIView):
class WorkspaceUserProfileEndpoint(BaseAPIView): class WorkspaceUserProfileEndpoint(BaseAPIView):
def get(self, request, slug, user_id): def get(self, request, slug, user_id):
try: try:
user_data = User.objects.get(pk=user_id) user_data = User.objects.get(pk=user_id)
requesting_workspace_member = WorkspaceMember.objects.get(workspace__slug=slug, member=request.user) requesting_workspace_member = WorkspaceMember.objects.get(
workspace__slug=slug, member=request.user
)
projects = [] projects = []
if requesting_workspace_member.role >= 10: if requesting_workspace_member.role >= 10:
projects = ( projects = (
@ -1227,7 +1231,8 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
) )
.annotate( .annotate(
created_issues=Count( created_issues=Count(
"project_issue", filter=Q(project_issue__created_by_id=user_id) "project_issue",
filter=Q(project_issue__created_by_id=user_id),
) )
) )
.annotate( .annotate(
@ -1282,6 +1287,7 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
"cover_image": user_data.cover_image, "cover_image": user_data.cover_image,
"date_joined": user_data.date_joined, "date_joined": user_data.date_joined,
"user_timezone": user_data.user_timezone, "user_timezone": user_data.user_timezone,
"display_name": user_data.display_name,
}, },
}, },
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
@ -1439,3 +1445,23 @@ class WorkspaceLabelsEndpoint(BaseAPIView):
{"error": "Something went wrong please try again later"}, {"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
class WorkspaceMembersEndpoint(BaseAPIView):
permission_classes = [
WorkspaceEntityPermission,
]
def get(self, request, slug):
try:
workspace_members = WorkspaceMember.objects.filter(
workspace__slug=slug
).select_related("workspace", "member")
serialzier = WorkSpaceMemberSerializer(workspace_members, many=True)
return Response(serialzier.data, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)

View File

@ -21,7 +21,7 @@ row_mapping = {
"state__name": "State", "state__name": "State",
"state__group": "State Group", "state__group": "State Group",
"labels__name": "Label", "labels__name": "Label",
"assignees__email": "Assignee Name", "assignees__display_name": "Assignee Name",
"start_date": "Start Date", "start_date": "Start Date",
"target_date": "Due Date", "target_date": "Due Date",
"completed_at": "Completed At", "completed_at": "Completed At",
@ -51,12 +51,12 @@ def analytic_export_task(email, data, slug):
segmented = segment segmented = segment
assignee_details = {} assignee_details = {}
if x_axis in ["assignees__email"] or segment in ["assignees__email"]: if x_axis in ["assignees__display_name"] or segment in ["assignees__display_name"]:
assignee_details = ( assignee_details = (
Issue.issue_objects.filter(workspace__slug=slug, **filters, assignees__avatar__isnull=False) Issue.issue_objects.filter(workspace__slug=slug, **filters, assignees__avatar__isnull=False)
.order_by("assignees__id") .order_by("assignees__id")
.distinct("assignees__id") .distinct("assignees__id")
.values("assignees__avatar", "assignees__email", "assignees__first_name", "assignees__last_name") .values("assignees__avatar", "assignees__display_name", "assignees__first_name", "assignees__last_name")
) )
if segment: if segment:
@ -93,17 +93,17 @@ def analytic_export_task(email, data, slug):
else: else:
generated_row.append("0") generated_row.append("0")
# x-axis replacement for names # x-axis replacement for names
if x_axis in ["assignees__email"]: if x_axis in ["assignees__display_name"]:
assignee = [user for user in assignee_details if str(user.get("assignees__email")) == str(item)] assignee = [user for user in assignee_details if str(user.get("assignees__display_name")) == str(item)]
if len(assignee): if len(assignee):
generated_row[0] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name")) generated_row[0] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name"))
rows.append(tuple(generated_row)) rows.append(tuple(generated_row))
# If segment is ["assignees__email"] then replace segment_zero rows with first and last names # If segment is ["assignees__display_name"] then replace segment_zero rows with first and last names
if segmented in ["assignees__email"]: if segmented in ["assignees__display_name"]:
for index, segm in enumerate(row_zero[2:]): for index, segm in enumerate(row_zero[2:]):
# find the name of the user # find the name of the user
assignee = [user for user in assignee_details if str(user.get("assignees__email")) == str(segm)] assignee = [user for user in assignee_details if str(user.get("assignees__display_name")) == str(segm)]
if len(assignee): if len(assignee):
row_zero[index] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name")) row_zero[index] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name"))
@ -141,8 +141,8 @@ def analytic_export_task(email, data, slug):
else distribution.get(item)[0].get("estimate "), else distribution.get(item)[0].get("estimate "),
] ]
# x-axis replacement to names # x-axis replacement to names
if x_axis in ["assignees__email"]: if x_axis in ["assignees__display_name"]:
assignee = [user for user in assignee_details if str(user.get("assignees__email")) == str(item)] assignee = [user for user in assignee_details if str(user.get("assignees__display_name")) == str(item)]
if len(assignee): if len(assignee):
row[0] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name")) row[0] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name"))

View File

@ -48,7 +48,7 @@ def track_name(
field="name", field="name",
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} updated the name to {requested_data.get('name')}", comment=f"updated the name to {requested_data.get('name')}",
) )
) )
@ -75,7 +75,7 @@ def track_parent(
field="parent", field="parent",
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} updated the parent issue to None", comment=f"updated the parent issue to None",
old_identifier=old_parent.id, old_identifier=old_parent.id,
new_identifier=None, new_identifier=None,
) )
@ -95,7 +95,7 @@ def track_parent(
field="parent", field="parent",
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} updated the parent issue to {new_parent.name}", comment=f"updated the parent issue to {new_parent.name}",
old_identifier=old_parent.id if old_parent is not None else None, old_identifier=old_parent.id if old_parent is not None else None,
new_identifier=new_parent.id, new_identifier=new_parent.id,
) )
@ -123,7 +123,7 @@ def track_priority(
field="priority", field="priority",
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} updated the priority to None", comment=f"updated the priority to None",
) )
) )
else: else:
@ -137,7 +137,7 @@ def track_priority(
field="priority", field="priority",
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} updated the priority to {requested_data.get('priority')}", comment=f"updated the priority to {requested_data.get('priority')}",
) )
) )
@ -165,7 +165,7 @@ def track_state(
field="state", field="state",
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} updated the state to {new_state.name}", comment=f"updated the state to {new_state.name}",
old_identifier=old_state.id, old_identifier=old_state.id,
new_identifier=new_state.id, new_identifier=new_state.id,
) )
@ -194,7 +194,7 @@ def track_description(
field="description", field="description",
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} updated the description to {requested_data.get('description_html')}", comment=f"updated the description to {requested_data.get('description_html')}",
) )
) )
@ -220,7 +220,7 @@ def track_target_date(
field="target_date", field="target_date",
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} updated the target date to None", comment=f"updated the target date to None",
) )
) )
else: else:
@ -234,7 +234,7 @@ def track_target_date(
field="target_date", field="target_date",
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} updated the target date to {requested_data.get('target_date')}", comment=f"updated the target date to {requested_data.get('target_date')}",
) )
) )
@ -260,7 +260,7 @@ def track_start_date(
field="start_date", field="start_date",
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} updated the start date to None", comment=f"updated the start date to None",
) )
) )
else: else:
@ -274,7 +274,7 @@ def track_start_date(
field="start_date", field="start_date",
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} updated the start date to {requested_data.get('start_date')}", comment=f"updated the start date to {requested_data.get('start_date')}",
) )
) )
@ -303,7 +303,7 @@ def track_labels(
field="labels", field="labels",
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} added label {label.name}", comment=f"added label {label.name}",
new_identifier=label.id, new_identifier=label.id,
old_identifier=None, old_identifier=None,
) )
@ -324,7 +324,7 @@ def track_labels(
field="labels", field="labels",
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} removed label {label.name}", comment=f"removed label {label.name}",
old_identifier=label.id, old_identifier=label.id,
new_identifier=None, new_identifier=None,
) )
@ -353,12 +353,12 @@ def track_assignees(
actor=actor, actor=actor,
verb="updated", verb="updated",
old_value="", old_value="",
new_value=assignee.email, new_value=assignee.display_name,
field="assignees", field="assignees",
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} added assignee {assignee.email}", comment=f"added assignee {assignee.display_name}",
new_identifier=actor.id, new_identifier=assignee.id,
) )
) )
@ -374,13 +374,13 @@ def track_assignees(
issue_id=issue_id, issue_id=issue_id,
actor=actor, actor=actor,
verb="updated", verb="updated",
old_value=assignee.email, old_value=assignee.display_name,
new_value="", new_value="",
field="assignees", field="assignees",
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} removed assignee {assignee.email}", comment=f"removed assignee {assignee.display_name}",
old_identifier=actor.id, old_identifier=assignee.id,
) )
) )
@ -419,7 +419,7 @@ def track_blocks(
field="blocks", field="blocks",
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} added blocking issue {issue.project.identifier}-{issue.sequence_id}", comment=f"added blocking issue {project.identifier}-{issue.sequence_id}",
new_identifier=issue.id, new_identifier=issue.id,
) )
) )
@ -441,7 +441,7 @@ def track_blocks(
field="blocks", field="blocks",
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} removed blocking issue {issue.project.identifier}-{issue.sequence_id}", comment=f"removed blocking issue {project.identifier}-{issue.sequence_id}",
old_identifier=issue.id, old_identifier=issue.id,
) )
) )
@ -481,7 +481,7 @@ def track_blockings(
field="blocking", field="blocking",
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} added blocked by issue {issue.project.identifier}-{issue.sequence_id}", comment=f"added blocked by issue {project.identifier}-{issue.sequence_id}",
new_identifier=issue.id, new_identifier=issue.id,
) )
) )
@ -503,7 +503,7 @@ def track_blockings(
field="blocking", field="blocking",
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} removed blocked by issue {issue.project.identifier}-{issue.sequence_id}", comment=f"removed blocked by issue {project.identifier}-{issue.sequence_id}",
old_identifier=issue.id, old_identifier=issue.id,
) )
) )
@ -517,7 +517,7 @@ def create_issue_activity(
issue_id=issue_id, issue_id=issue_id,
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} created the issue", comment=f"created the issue",
verb="created", verb="created",
actor=actor, actor=actor,
) )
@ -539,7 +539,7 @@ def track_estimate_points(
field="estimate_point", field="estimate_point",
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} updated the estimate point to None", comment=f"updated the estimate point to None",
) )
) )
else: else:
@ -553,7 +553,7 @@ def track_estimate_points(
field="estimate_point", field="estimate_point",
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} updated the estimate point to {requested_data.get('estimate_point')}", comment=f"updated the estimate point to {requested_data.get('estimate_point')}",
) )
) )
@ -567,7 +567,7 @@ def track_archive_at(
issue_id=issue_id, issue_id=issue_id,
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} has restored the issue", comment=f"has restored the issue",
verb="updated", verb="updated",
actor=actor, actor=actor,
field="archived_at", field="archived_at",
@ -661,7 +661,7 @@ def delete_issue_activity(
IssueActivity( IssueActivity(
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} deleted the issue", comment=f"deleted the issue",
verb="deleted", verb="deleted",
actor=actor, actor=actor,
field="issue", field="issue",
@ -682,7 +682,7 @@ def create_comment_activity(
issue_id=issue_id, issue_id=issue_id,
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} created a comment", comment=f"created a comment",
verb="created", verb="created",
actor=actor, actor=actor,
field="comment", field="comment",
@ -707,7 +707,7 @@ def update_comment_activity(
issue_id=issue_id, issue_id=issue_id,
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} updated a comment", comment=f"updated a comment",
verb="updated", verb="updated",
actor=actor, actor=actor,
field="comment", field="comment",
@ -728,7 +728,7 @@ def delete_comment_activity(
issue_id=issue_id, issue_id=issue_id,
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} deleted the comment", comment=f"deleted the comment",
verb="deleted", verb="deleted",
actor=actor, actor=actor,
field="comment", field="comment",
@ -766,7 +766,7 @@ def create_cycle_issue_activity(
field="cycles", field="cycles",
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} updated cycle from {old_cycle.name} to {new_cycle.name}", comment=f"updated cycle from {old_cycle.name} to {new_cycle.name}",
old_identifier=old_cycle.id, old_identifier=old_cycle.id,
new_identifier=new_cycle.id, new_identifier=new_cycle.id,
) )
@ -787,7 +787,7 @@ def create_cycle_issue_activity(
field="cycles", field="cycles",
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} added cycle {cycle.name}", comment=f"added cycle {cycle.name}",
new_identifier=cycle.id, new_identifier=cycle.id,
) )
) )
@ -816,7 +816,7 @@ def delete_cycle_issue_activity(
field="cycles", field="cycles",
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} removed this issue from {cycle.name if cycle is not None else None}", comment=f"removed this issue from {cycle.name if cycle is not None else None}",
old_identifier=cycle.id if cycle is not None else None, old_identifier=cycle.id if cycle is not None else None,
) )
) )
@ -852,7 +852,7 @@ def create_module_issue_activity(
field="modules", field="modules",
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} updated module from {old_module.name} to {new_module.name}", comment=f"updated module from {old_module.name} to {new_module.name}",
old_identifier=old_module.id, old_identifier=old_module.id,
new_identifier=new_module.id, new_identifier=new_module.id,
) )
@ -872,7 +872,7 @@ def create_module_issue_activity(
field="modules", field="modules",
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} added module {module.name}", comment=f"added module {module.name}",
new_identifier=module.id, new_identifier=module.id,
) )
) )
@ -901,7 +901,7 @@ def delete_module_issue_activity(
field="modules", field="modules",
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} removed this issue from {module.name if module is not None else None}", comment=f"removed this issue from {module.name if module is not None else None}",
old_identifier=module.id if module is not None else None, old_identifier=module.id if module is not None else None,
) )
) )
@ -920,7 +920,7 @@ def create_link_activity(
issue_id=issue_id, issue_id=issue_id,
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} created a link", comment=f"created a link",
verb="created", verb="created",
actor=actor, actor=actor,
field="link", field="link",
@ -944,7 +944,7 @@ def update_link_activity(
issue_id=issue_id, issue_id=issue_id,
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} updated a link", comment=f"updated a link",
verb="updated", verb="updated",
actor=actor, actor=actor,
field="link", field="link",
@ -969,7 +969,7 @@ def delete_link_activity(
issue_id=issue_id, issue_id=issue_id,
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} deleted the link", comment=f"deleted the link",
verb="deleted", verb="deleted",
actor=actor, actor=actor,
field="link", field="link",
@ -992,7 +992,7 @@ def create_attachment_activity(
issue_id=issue_id, issue_id=issue_id,
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} created an attachment", comment=f"created an attachment",
verb="created", verb="created",
actor=actor, actor=actor,
field="attachment", field="attachment",
@ -1010,7 +1010,7 @@ def delete_attachment_activity(
issue_id=issue_id, issue_id=issue_id,
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f"{actor.email} deleted the attachment", comment=f"deleted the attachment",
verb="deleted", verb="deleted",
actor=actor, actor=actor,
field="attachment", field="attachment",

View File

@ -0,0 +1,101 @@
# Generated by Django 4.2.3 on 2023-08-04 09:12
import string
import random
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
def generate_display_name(apps, schema_editor):
UserModel = apps.get_model("db", "User")
updated_users = []
for obj in UserModel.objects.all():
obj.display_name = (
obj.email.split("@")[0]
if len(obj.email.split("@"))
else "".join(random.choice(string.ascii_letters) for _ in range(6))
)
updated_users.append(obj)
UserModel.objects.bulk_update(updated_users, ["display_name"], batch_size=100)
def rectify_field_issue_activity(apps, schema_editor):
Model = apps.get_model("db", "IssueActivity")
updated_activity = []
for obj in Model.objects.filter(field="assignee"):
obj.field = "assignees"
updated_activity.append(obj)
Model.objects.bulk_update(updated_activity, ["field"], batch_size=100)
def update_assignee_issue_activity(apps, schema_editor):
Model = apps.get_model("db", "IssueActivity")
updated_activity = []
# Get all the users
User = apps.get_model("db", "User")
users = User.objects.values("id", "email", "display_name")
for obj in Model.objects.filter(field="assignees"):
if bool(obj.new_value) and not bool(obj.old_value):
# Get user from list
assigned_user = [
user for user in users if user.get("email") == obj.new_value
]
if assigned_user:
obj.new_value = assigned_user[0].get("display_name")
obj.new_identifier = assigned_user[0].get("id")
# Update the comment
words = obj.comment.split()
words[-1] = assigned_user[0].get("display_name")
obj.comment = " ".join(words)
if bool(obj.old_value) and not bool(obj.new_value):
# Get user from list
assigned_user = [
user for user in users if user.get("email") == obj.old_value
]
if assigned_user:
obj.old_value = assigned_user[0].get("display_name")
obj.old_identifier = assigned_user[0].get("id")
# Update the comment
words = obj.comment.split()
words[-1] = assigned_user[0].get("display_name")
obj.comment = " ".join(words)
updated_activity.append(obj)
Model.objects.bulk_update(
updated_activity,
["old_value", "new_value", "old_identifier", "new_identifier", "comment"],
batch_size=200,
)
def update_name_activity(apps, schema_editor):
Model = apps.get_model("db", "IssueActivity")
update_activity = []
for obj in Model.objects.filter(field="name"):
obj.comment = obj.comment.replace("start date", "name")
update_activity.append(obj)
Model.objects.bulk_update(update_activity, ["comment"], batch_size=1000)
class Migration(migrations.Migration):
dependencies = [
("db", "0040_projectmember_preferences_user_cover_image_and_more"),
]
operations = [
migrations.AddField(
model_name="user",
name="display_name",
field=models.CharField(default="", max_length=255),
),
migrations.RunPython(generate_display_name),
migrations.RunPython(rectify_field_issue_activity),
migrations.RunPython(update_assignee_issue_activity),
migrations.RunPython(update_name_activity),
]

View File

@ -1,6 +1,7 @@
# Python imports # Python imports
from enum import unique
import uuid import uuid
import string
import random
# Django imports # Django imports
from django.db import models from django.db import models
@ -18,6 +19,7 @@ from sentry_sdk import capture_exception
from slack_sdk import WebClient from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError from slack_sdk.errors import SlackApiError
def get_default_onboarding(): def get_default_onboarding():
return { return {
"profile_complete": False, "profile_complete": False,
@ -26,6 +28,7 @@ def get_default_onboarding():
"workspace_join": False, "workspace_join": False,
} }
class User(AbstractBaseUser, PermissionsMixin): class User(AbstractBaseUser, PermissionsMixin):
id = models.UUIDField( id = models.UUIDField(
default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True
@ -81,6 +84,7 @@ class User(AbstractBaseUser, PermissionsMixin):
role = models.CharField(max_length=300, null=True, blank=True) role = models.CharField(max_length=300, null=True, blank=True)
is_bot = models.BooleanField(default=False) is_bot = models.BooleanField(default=False)
theme = models.JSONField(default=dict) theme = models.JSONField(default=dict)
display_name = models.CharField(max_length=255, default="")
is_tour_completed = models.BooleanField(default=False) is_tour_completed = models.BooleanField(default=False)
onboarding_step = models.JSONField(default=get_default_onboarding) onboarding_step = models.JSONField(default=get_default_onboarding)
@ -107,6 +111,13 @@ class User(AbstractBaseUser, PermissionsMixin):
self.token = uuid.uuid4().hex + uuid.uuid4().hex self.token = uuid.uuid4().hex + uuid.uuid4().hex
self.token_updated_at = timezone.now() self.token_updated_at = timezone.now()
if not self.display_name:
self.display_name = (
self.email.split("@")[0]
if len(self.email.split("@"))
else "".join(random.choice(string.ascii_letters) for _ in range(6))
)
if self.is_superuser: if self.is_superuser:
self.is_staff = True self.is_staff = True