From cf306ee605880ac549333c17d47f6d0dda461255 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Tue, 8 Aug 2023 12:59:04 +0530 Subject: [PATCH] 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 --- apiserver/bin/user_script.py | 8 +- apiserver/plane/api/serializers/__init__.py | 8 +- apiserver/plane/api/serializers/people.py | 57 ---------- apiserver/plane/api/serializers/project.py | 13 ++- apiserver/plane/api/serializers/user.py | 44 +++++++- apiserver/plane/api/serializers/workspace.py | 35 +++--- apiserver/plane/api/urls.py | 12 +++ apiserver/plane/api/views/__init__.py | 4 +- apiserver/plane/api/views/analytic.py | 10 +- apiserver/plane/api/views/auth_extended.py | 2 +- apiserver/plane/api/views/importer.py | 2 +- apiserver/plane/api/views/page.py | 2 +- apiserver/plane/api/views/project.py | 24 ++++- .../plane/api/views/{people.py => user.py} | 0 apiserver/plane/api/views/workspace.py | 48 +++++++-- .../plane/bgtasks/analytic_plot_export.py | 20 ++-- .../plane/bgtasks/issue_activites_task.py | 84 +++++++-------- ..._alter_analyticview_created_by_and_more.py | 101 ++++++++++++++++++ apiserver/plane/db/models/user.py | 13 ++- 19 files changed, 330 insertions(+), 157 deletions(-) delete mode 100644 apiserver/plane/api/serializers/people.py rename apiserver/plane/api/views/{people.py => user.py} (100%) create mode 100644 apiserver/plane/db/migrations/0041_user_display_name_alter_analyticview_created_by_and_more.py diff --git a/apiserver/bin/user_script.py b/apiserver/bin/user_script.py index b554d2c405e..e115b20b8c0 100644 --- a/apiserver/bin/user_script.py +++ b/apiserver/bin/user_script.py @@ -1,4 +1,4 @@ -import os, sys +import os, sys, random, string import uuid sys.path.append("/code") @@ -19,9 +19,9 @@ def populate(): user = User.objects.create(email=default_email, username=uuid.uuid4().hex) user.set_password(default_password) user.save() - print("User created") - - print("Success") + print(f"User created with an email: {default_email}") + else: + print(f"User already exists with the default email: {default_email}") if __name__ == "__main__": diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py index 2d38b1139a9..683ed9670db 100644 --- a/apiserver/plane/api/serializers/__init__.py +++ b/apiserver/plane/api/serializers/__init__.py @@ -1,10 +1,5 @@ from .base import BaseSerializer -from .people import ( - ChangePasswordSerializer, - ResetPasswordSerializer, - TokenSerializer, -) -from .user import UserSerializer, UserLiteSerializer +from .user import UserSerializer, UserLiteSerializer, ChangePasswordSerializer, ResetPasswordSerializer, UserAdminLiteSerializer from .workspace import ( WorkSpaceSerializer, WorkSpaceMemberSerializer, @@ -12,6 +7,7 @@ WorkSpaceMemberInviteSerializer, WorkspaceLiteSerializer, WorkspaceThemeSerializer, + WorkspaceMemberAdminSerializer, ) from .project import ( ProjectSerializer, diff --git a/apiserver/plane/api/serializers/people.py b/apiserver/plane/api/serializers/people.py deleted file mode 100644 index b8b59416cc7..00000000000 --- a/apiserver/plane/api/serializers/people.py +++ /dev/null @@ -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__" diff --git a/apiserver/plane/api/serializers/project.py b/apiserver/plane/api/serializers/project.py index fa97c5a6ddf..643518daa10 100644 --- a/apiserver/plane/api/serializers/project.py +++ b/apiserver/plane/api/serializers/project.py @@ -7,7 +7,7 @@ # Module imports from .base import BaseSerializer 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 ( Project, ProjectMember, @@ -110,6 +110,17 @@ class Meta: 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): project = ProjectLiteSerializer(read_only=True) workspace = WorkspaceLiteSerializer(read_only=True) diff --git a/apiserver/plane/api/serializers/user.py b/apiserver/plane/api/serializers/user.py index d8978479e84..dcb00c6cbfe 100644 --- a/apiserver/plane/api/serializers/user.py +++ b/apiserver/plane/api/serializers/user.py @@ -1,3 +1,6 @@ +# Third party imports +from rest_framework import serializers + # Module import from .base import BaseSerializer from plane.db.models import User @@ -37,11 +40,50 @@ class Meta: "id", "first_name", "last_name", - "email", "avatar", "is_bot", + "display_name", ] read_only_fields = [ "id", "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) diff --git a/apiserver/plane/api/serializers/workspace.py b/apiserver/plane/api/serializers/workspace.py index 4d83d6262a9..d27b66481c0 100644 --- a/apiserver/plane/api/serializers/workspace.py +++ b/apiserver/plane/api/serializers/workspace.py @@ -3,7 +3,7 @@ # Module imports from .base import BaseSerializer -from .user import UserLiteSerializer +from .user import UserLiteSerializer, UserAdminLiteSerializer from plane.db.models import ( User, @@ -33,10 +33,30 @@ class Meta: "owner", ] +class WorkspaceLiteSerializer(BaseSerializer): + class Meta: + model = Workspace + fields = [ + "name", + "slug", + "id", + ] + read_only_fields = fields + + class WorkSpaceMemberSerializer(BaseSerializer): 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: model = WorkspaceMember @@ -101,17 +121,6 @@ def update(self, 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 Meta: model = WorkspaceTheme diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 2d1b2c9082f..b1231f1a409 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -32,6 +32,7 @@ InviteWorkspaceEndpoint, JoinWorkspaceEndpoint, WorkSpaceMemberViewSet, + WorkspaceMembersEndpoint, WorkspaceInvitationsViewset, UserWorkspaceInvitationsEndpoint, WorkspaceMemberUserEndpoint, @@ -59,6 +60,7 @@ ProjectViewSet, InviteProjectEndpoint, ProjectMemberViewSet, + ProjectMemberEndpoint, ProjectMemberInvitationsViewset, ProjectMemberUserEndpoint, AddMemberToProjectEndpoint, @@ -335,6 +337,11 @@ ), name="workspace", ), + path( + "workspaces//workspace-members/", + WorkspaceMembersEndpoint.as_view(), + name="workspace-members", + ), path( "workspaces//teams/", TeamMemberViewSet.as_view( @@ -468,6 +475,11 @@ ), name="project", ), + path( + "workspaces//projects//project-members/", + ProjectMemberEndpoint.as_view(), + name="project", + ), path( "workspaces//projects//members/add/", AddMemberToProjectEndpoint.as_view(), diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 92b647a972d..a02e22fe9cf 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -12,8 +12,9 @@ ProjectUserViewsEndpoint, ProjectMemberUserEndpoint, ProjectFavoritesViewSet, + ProjectMemberEndpoint, ) -from .people import ( +from .user import ( UserEndpoint, UpdateUserOnBoardedEndpoint, UpdateUserTourCompletedEndpoint, @@ -47,6 +48,7 @@ WorkspaceUserProfileEndpoint, WorkspaceUserProfileIssuesEndpoint, WorkspaceLabelsEndpoint, + WorkspaceMembersEndpoint, ) from .state import StateViewSet from .view import IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet diff --git a/apiserver/plane/api/views/analytic.py b/apiserver/plane/api/views/analytic.py index e537af84a82..7d5786c19ef 100644 --- a/apiserver/plane/api/views/analytic.py +++ b/apiserver/plane/api/views/analytic.py @@ -79,12 +79,12 @@ def get(self, request, slug): ) 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 = ( Issue.issue_objects.filter(workspace__slug=slug, **filters, assignees__avatar__isnull=False) .order_by("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 @@ def get(self, request, slug): ) most_issue_created_user = ( 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")) .order_by("-count") )[:5] most_issue_closed_user = ( 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")) .order_by("-count") )[:5] pending_issue_user = ( 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")) .order_by("-count") ) diff --git a/apiserver/plane/api/views/auth_extended.py b/apiserver/plane/api/views/auth_extended.py index 56dc091f489..df3f3aaca83 100644 --- a/apiserver/plane/api/views/auth_extended.py +++ b/apiserver/plane/api/views/auth_extended.py @@ -22,7 +22,7 @@ ## Module imports from . import BaseAPIView -from plane.api.serializers.people import ( +from plane.api.serializers import ( ChangePasswordSerializer, ResetPasswordSerializer, ) diff --git a/apiserver/plane/api/views/importer.py b/apiserver/plane/api/views/importer.py index 4fc7ad48357..0a92b3850fe 100644 --- a/apiserver/plane/api/views/importer.py +++ b/apiserver/plane/api/views/importer.py @@ -458,7 +458,7 @@ def post(self, request, slug, project_id, service): actor=request.user, project_id=project_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", created_by=request.user, ) diff --git a/apiserver/plane/api/views/page.py b/apiserver/plane/api/views/page.py index edca47ffe39..d9fad9eaa95 100644 --- a/apiserver/plane/api/views/page.py +++ b/apiserver/plane/api/views/page.py @@ -301,7 +301,7 @@ def post(self, request, slug, project_id, page_id, page_block_id): issue=issue, actor=request.user, 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", ) diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py index 31741f10c16..98484f74bff 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -25,7 +25,7 @@ ProjectFavoriteSerializer, ) -from plane.api.permissions import ProjectBasePermission +from plane.api.permissions import ProjectBasePermission, ProjectEntityPermission from plane.db.models import ( Project, @@ -458,7 +458,7 @@ class ProjectMemberViewSet(BaseViewSet): ] search_fields = [ - "member__email", + "member__display_name", "member__first_name", ] @@ -984,3 +984,23 @@ def destroy(self, request, slug, project_id): {"error": "Something went wrong please try again later"}, 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, + ) diff --git a/apiserver/plane/api/views/people.py b/apiserver/plane/api/views/user.py similarity index 100% rename from apiserver/plane/api/views/people.py rename to apiserver/plane/api/views/user.py diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index a862c0b4c3f..3957bcae0a5 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -47,6 +47,7 @@ WorkspaceThemeSerializer, IssueActivitySerializer, IssueLiteSerializer, + WorkspaceMemberAdminSerializer ) from plane.api.views.base import BaseAPIView from . import BaseViewSet @@ -537,7 +538,7 @@ def create(self, request): class WorkSpaceMemberViewSet(BaseViewSet): - serializer_class = WorkSpaceMemberSerializer + serializer_class = WorkspaceMemberAdminSerializer model = WorkspaceMember permission_classes = [ @@ -545,7 +546,7 @@ class WorkSpaceMemberViewSet(BaseViewSet): ] search_fields = [ - "member__email", + "member__display_name", "member__first_name", ] @@ -690,7 +691,7 @@ class TeamMemberViewSet(BaseViewSet): ] search_fields = [ - "member__email", + "member__display_name", "member__first_name", ] @@ -1048,7 +1049,6 @@ def create(self, request, slug): class WorkspaceUserProfileStatsEndpoint(BaseAPIView): - def get(self, request, slug, user_id): try: filters = issue_filters(request.query_params, "GET") @@ -1146,14 +1146,18 @@ def get(self, request, slug, user_id): upcoming_cycles = CycleIssue.objects.filter( workspace__slug=slug, 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") present_cycle = CycleIssue.objects.filter( workspace__slug=slug, cycle__start_date__lt=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") return Response( @@ -1166,7 +1170,7 @@ def get(self, request, slug, user_id): "pending_issues": pending_issues_count, "subscribed_issues": subscribed_issues_count, "present_cycles": present_cycle, - "upcoming_cycles": upcoming_cycles, + "upcoming_cycles": upcoming_cycles, } ) except Exception as e: @@ -1184,7 +1188,6 @@ class WorkspaceUserActivityEndpoint(BaseAPIView): def get(self, request, slug, user_id): try: - projects = request.query_params.getlist("project", []) queryset = IssueActivity.objects.filter( @@ -1212,12 +1215,13 @@ def get(self, request, slug, user_id): class WorkspaceUserProfileEndpoint(BaseAPIView): - def get(self, request, slug, user_id): try: 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 = [] if requesting_workspace_member.role >= 10: projects = ( @@ -1227,7 +1231,8 @@ def get(self, request, slug, user_id): ) .annotate( 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( @@ -1282,6 +1287,7 @@ def get(self, request, slug, user_id): "cover_image": user_data.cover_image, "date_joined": user_data.date_joined, "user_timezone": user_data.user_timezone, + "display_name": user_data.display_name, }, }, status=status.HTTP_200_OK, @@ -1439,3 +1445,23 @@ def get(self, request, slug): {"error": "Something went wrong please try again later"}, 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, + ) diff --git a/apiserver/plane/bgtasks/analytic_plot_export.py b/apiserver/plane/bgtasks/analytic_plot_export.py index 27b625445ad..3177c39fe7e 100644 --- a/apiserver/plane/bgtasks/analytic_plot_export.py +++ b/apiserver/plane/bgtasks/analytic_plot_export.py @@ -21,7 +21,7 @@ "state__name": "State", "state__group": "State Group", "labels__name": "Label", - "assignees__email": "Assignee Name", + "assignees__display_name": "Assignee Name", "start_date": "Start Date", "target_date": "Due Date", "completed_at": "Completed At", @@ -51,12 +51,12 @@ def analytic_export_task(email, data, slug): segmented = segment 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 = ( Issue.issue_objects.filter(workspace__slug=slug, **filters, assignees__avatar__isnull=False) .order_by("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: @@ -93,17 +93,17 @@ def analytic_export_task(email, data, slug): else: generated_row.append("0") # x-axis replacement for names - if x_axis in ["assignees__email"]: - assignee = [user for user in assignee_details if str(user.get("assignees__email")) == str(item)] + if x_axis in ["assignees__display_name"]: + assignee = [user for user in assignee_details if str(user.get("assignees__display_name")) == str(item)] if len(assignee): generated_row[0] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name")) rows.append(tuple(generated_row)) - # If segment is ["assignees__email"] then replace segment_zero rows with first and last names - if segmented in ["assignees__email"]: + # If segment is ["assignees__display_name"] then replace segment_zero rows with first and last names + if segmented in ["assignees__display_name"]: for index, segm in enumerate(row_zero[2:]): # 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): 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 "), ] # x-axis replacement to names - if x_axis in ["assignees__email"]: - assignee = [user for user in assignee_details if str(user.get("assignees__email")) == str(item)] + if x_axis in ["assignees__display_name"]: + assignee = [user for user in assignee_details if str(user.get("assignees__display_name")) == str(item)] if len(assignee): row[0] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name")) diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index 8f34daf52f3..9150d7c9478 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -48,7 +48,7 @@ def track_name( field="name", project=project, 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", project=project, 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, new_identifier=None, ) @@ -95,7 +95,7 @@ def track_parent( field="parent", project=project, 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, new_identifier=new_parent.id, ) @@ -123,7 +123,7 @@ def track_priority( field="priority", project=project, workspace=project.workspace, - comment=f"{actor.email} updated the priority to None", + comment=f"updated the priority to None", ) ) else: @@ -137,7 +137,7 @@ def track_priority( field="priority", project=project, 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", project=project, 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, new_identifier=new_state.id, ) @@ -194,7 +194,7 @@ def track_description( field="description", project=project, 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", project=project, workspace=project.workspace, - comment=f"{actor.email} updated the target date to None", + comment=f"updated the target date to None", ) ) else: @@ -234,7 +234,7 @@ def track_target_date( field="target_date", project=project, 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", project=project, workspace=project.workspace, - comment=f"{actor.email} updated the start date to None", + comment=f"updated the start date to None", ) ) else: @@ -274,7 +274,7 @@ def track_start_date( field="start_date", project=project, 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", project=project, workspace=project.workspace, - comment=f"{actor.email} added label {label.name}", + comment=f"added label {label.name}", new_identifier=label.id, old_identifier=None, ) @@ -324,7 +324,7 @@ def track_labels( field="labels", project=project, workspace=project.workspace, - comment=f"{actor.email} removed label {label.name}", + comment=f"removed label {label.name}", old_identifier=label.id, new_identifier=None, ) @@ -353,12 +353,12 @@ def track_assignees( actor=actor, verb="updated", old_value="", - new_value=assignee.email, + new_value=assignee.display_name, field="assignees", project=project, workspace=project.workspace, - comment=f"{actor.email} added assignee {assignee.email}", - new_identifier=actor.id, + comment=f"added assignee {assignee.display_name}", + new_identifier=assignee.id, ) ) @@ -374,13 +374,13 @@ def track_assignees( issue_id=issue_id, actor=actor, verb="updated", - old_value=assignee.email, + old_value=assignee.display_name, new_value="", field="assignees", project=project, workspace=project.workspace, - comment=f"{actor.email} removed assignee {assignee.email}", - old_identifier=actor.id, + comment=f"removed assignee {assignee.display_name}", + old_identifier=assignee.id, ) ) @@ -419,7 +419,7 @@ def track_blocks( field="blocks", project=project, 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, ) ) @@ -441,7 +441,7 @@ def track_blocks( field="blocks", project=project, 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, ) ) @@ -481,7 +481,7 @@ def track_blockings( field="blocking", project=project, 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, ) ) @@ -503,7 +503,7 @@ def track_blockings( field="blocking", project=project, 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, ) ) @@ -517,7 +517,7 @@ def create_issue_activity( issue_id=issue_id, project=project, workspace=project.workspace, - comment=f"{actor.email} created the issue", + comment=f"created the issue", verb="created", actor=actor, ) @@ -539,7 +539,7 @@ def track_estimate_points( field="estimate_point", project=project, workspace=project.workspace, - comment=f"{actor.email} updated the estimate point to None", + comment=f"updated the estimate point to None", ) ) else: @@ -553,7 +553,7 @@ def track_estimate_points( field="estimate_point", project=project, 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, project=project, workspace=project.workspace, - comment=f"{actor.email} has restored the issue", + comment=f"has restored the issue", verb="updated", actor=actor, field="archived_at", @@ -661,7 +661,7 @@ def delete_issue_activity( IssueActivity( project=project, workspace=project.workspace, - comment=f"{actor.email} deleted the issue", + comment=f"deleted the issue", verb="deleted", actor=actor, field="issue", @@ -682,7 +682,7 @@ def create_comment_activity( issue_id=issue_id, project=project, workspace=project.workspace, - comment=f"{actor.email} created a comment", + comment=f"created a comment", verb="created", actor=actor, field="comment", @@ -707,7 +707,7 @@ def update_comment_activity( issue_id=issue_id, project=project, workspace=project.workspace, - comment=f"{actor.email} updated a comment", + comment=f"updated a comment", verb="updated", actor=actor, field="comment", @@ -728,7 +728,7 @@ def delete_comment_activity( issue_id=issue_id, project=project, workspace=project.workspace, - comment=f"{actor.email} deleted the comment", + comment=f"deleted the comment", verb="deleted", actor=actor, field="comment", @@ -766,7 +766,7 @@ def create_cycle_issue_activity( field="cycles", project=project, 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, new_identifier=new_cycle.id, ) @@ -787,7 +787,7 @@ def create_cycle_issue_activity( field="cycles", project=project, workspace=project.workspace, - comment=f"{actor.email} added cycle {cycle.name}", + comment=f"added cycle {cycle.name}", new_identifier=cycle.id, ) ) @@ -816,7 +816,7 @@ def delete_cycle_issue_activity( field="cycles", project=project, 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, ) ) @@ -852,7 +852,7 @@ def create_module_issue_activity( field="modules", project=project, 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, new_identifier=new_module.id, ) @@ -872,7 +872,7 @@ def create_module_issue_activity( field="modules", project=project, workspace=project.workspace, - comment=f"{actor.email} added module {module.name}", + comment=f"added module {module.name}", new_identifier=module.id, ) ) @@ -901,7 +901,7 @@ def delete_module_issue_activity( field="modules", project=project, 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, ) ) @@ -920,7 +920,7 @@ def create_link_activity( issue_id=issue_id, project=project, workspace=project.workspace, - comment=f"{actor.email} created a link", + comment=f"created a link", verb="created", actor=actor, field="link", @@ -944,7 +944,7 @@ def update_link_activity( issue_id=issue_id, project=project, workspace=project.workspace, - comment=f"{actor.email} updated a link", + comment=f"updated a link", verb="updated", actor=actor, field="link", @@ -969,7 +969,7 @@ def delete_link_activity( issue_id=issue_id, project=project, workspace=project.workspace, - comment=f"{actor.email} deleted the link", + comment=f"deleted the link", verb="deleted", actor=actor, field="link", @@ -992,7 +992,7 @@ def create_attachment_activity( issue_id=issue_id, project=project, workspace=project.workspace, - comment=f"{actor.email} created an attachment", + comment=f"created an attachment", verb="created", actor=actor, field="attachment", @@ -1010,7 +1010,7 @@ def delete_attachment_activity( issue_id=issue_id, project=project, workspace=project.workspace, - comment=f"{actor.email} deleted the attachment", + comment=f"deleted the attachment", verb="deleted", actor=actor, field="attachment", diff --git a/apiserver/plane/db/migrations/0041_user_display_name_alter_analyticview_created_by_and_more.py b/apiserver/plane/db/migrations/0041_user_display_name_alter_analyticview_created_by_and_more.py new file mode 100644 index 00000000000..0a561db5ead --- /dev/null +++ b/apiserver/plane/db/migrations/0041_user_display_name_alter_analyticview_created_by_and_more.py @@ -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), + ] diff --git a/apiserver/plane/db/models/user.py b/apiserver/plane/db/models/user.py index 0b643271e64..3975a3b9311 100644 --- a/apiserver/plane/db/models/user.py +++ b/apiserver/plane/db/models/user.py @@ -1,6 +1,7 @@ # Python imports -from enum import unique import uuid +import string +import random # Django imports from django.db import models @@ -18,6 +19,7 @@ from slack_sdk import WebClient from slack_sdk.errors import SlackApiError + def get_default_onboarding(): return { "profile_complete": False, @@ -26,6 +28,7 @@ def get_default_onboarding(): "workspace_join": False, } + class User(AbstractBaseUser, PermissionsMixin): id = models.UUIDField( 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) is_bot = models.BooleanField(default=False) theme = models.JSONField(default=dict) + display_name = models.CharField(max_length=255, default="") is_tour_completed = models.BooleanField(default=False) onboarding_step = models.JSONField(default=get_default_onboarding) @@ -107,6 +111,13 @@ def save(self, *args, **kwargs): self.token = uuid.uuid4().hex + uuid.uuid4().hex 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: self.is_staff = True