Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WEB-373] chore: new dashboard updates #3849

Merged
merged 39 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
dc333d0
chore: replaced marimekko graph with a bar graph
aaryan610 Feb 16, 2024
6b7f3c7
chore: add bar onClick handler
aaryan610 Feb 16, 2024
1b25b4e
chore: custom date filter for widgets
aaryan610 Feb 16, 2024
e6ca4ff
style: priority graph
aaryan610 Feb 16, 2024
b04f303
chore: workspace profile activity pagination
aaryan610 Feb 17, 2024
fc29787
Merge branch 'develop' of https://github.com/makeplane/plane into cho…
aaryan610 Feb 17, 2024
f244a14
chore: profile activity pagination
aaryan610 Feb 19, 2024
14a07be
chore: user profile activity pagination
aaryan610 Feb 19, 2024
e861161
chore: workspace user activity csv download
NarayanBavisetti Feb 19, 2024
d746bb3
Merge branch 'develop' of github.com:makeplane/plane into chore/dashb…
NarayanBavisetti Feb 19, 2024
6f552ca
Merge branch 'chore/dashboard-improvement' of github.com:makeplane/pl…
NarayanBavisetti Feb 19, 2024
8ef7ba6
Merge branch 'develop' of https://github.com/makeplane/plane into cho…
aaryan610 Feb 19, 2024
a4c1c29
Merge branch 'chore/dashboard-improvement' of https://github.com/make…
aaryan610 Feb 19, 2024
9de3f7a
chore: download activity button added
aaryan610 Feb 19, 2024
c142996
chore: workspace user pagination
NarayanBavisetti Feb 19, 2024
a80838c
Merge branch 'chore/dashboard-improvement' of github.com:makeplane/pl…
NarayanBavisetti Feb 19, 2024
46ec16f
chore: collabrator pagination
NarayanBavisetti Feb 20, 2024
bfe384a
chore: field change
NarayanBavisetti Feb 20, 2024
862c22e
chore: recent collaborators pagination
aaryan610 Feb 20, 2024
56bb747
chore: changed the collabrators
NarayanBavisetti Feb 20, 2024
e8b4724
Merge branch 'chore/dashboard-improvement' of github.com:makeplane/pl…
NarayanBavisetti Feb 21, 2024
1f3df62
chore: collabrators list changed
NarayanBavisetti Feb 21, 2024
ebe02fc
fix: distinct users
NarayanBavisetti Feb 22, 2024
f74b411
chore: search filter in collaborators
NarayanBavisetti Feb 23, 2024
4725051
fix: merge conflicts resolved from develop
aaryan610 Feb 23, 2024
45c15d6
fix: merge conflicts resolved from develop
aaryan610 Feb 23, 2024
d988d4b
fix: import error
aaryan610 Feb 23, 2024
e09d9f2
fix: merge conflicts resolved from develop
aaryan610 Feb 29, 2024
4622cf5
chore: update priority graph x-axis values
aaryan610 Mar 1, 2024
6ac55e1
chore: admin and member request validation
NarayanBavisetti Mar 1, 2024
ec471c7
Merge branch 'chore/dashboard-improvement' of github.com:makeplane/pl…
NarayanBavisetti Mar 1, 2024
7c28984
chore: update csv download request method
aaryan610 Mar 3, 2024
a5f04db
Merge branch 'develop' of https://github.com/makeplane/plane into cho…
aaryan610 Mar 5, 2024
fb13cfb
chore: search implementation for the collaborators widget
aaryan610 Mar 5, 2024
9a53003
refactor: priority distribution card
aaryan610 Mar 5, 2024
29f06ca
chore: add enum for duration filters
aaryan610 Mar 5, 2024
d4afec9
chore: update inbox types
aaryan610 Mar 6, 2024
a6bf381
chore: add todos for refactoring
aaryan610 Mar 6, 2024
0a1183a
fix: merge conflicts resolved from develop
aaryan610 Mar 6, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apiserver/plane/app/urls/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
WorkspaceUserPropertiesEndpoint,
WorkspaceStatesEndpoint,
WorkspaceEstimatesEndpoint,
ExportWorkspaceUserActivityEndpoint,
WorkspaceModulesEndpoint,
WorkspaceCyclesEndpoint,
)
Expand Down Expand Up @@ -191,6 +192,11 @@
WorkspaceUserActivityEndpoint.as_view(),
name="workspace-user-activity",
),
path(
"workspaces/<str:slug>/user-activity/<uuid:user_id>/export/",
ExportWorkspaceUserActivityEndpoint.as_view(),
name="export-workspace-user-activity",
),
path(
"workspaces/<str:slug>/user-profile/<uuid:user_id>/",
WorkspaceUserProfileEndpoint.as_view(),
Expand Down
1 change: 1 addition & 0 deletions apiserver/plane/app/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
WorkspaceUserPropertiesEndpoint,
WorkspaceStatesEndpoint,
WorkspaceEstimatesEndpoint,
ExportWorkspaceUserActivityEndpoint,
WorkspaceModulesEndpoint,
WorkspaceCyclesEndpoint,
)
Expand Down
180 changes: 98 additions & 82 deletions apiserver/plane/app/views/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
JSONField,
Func,
Prefetch,
IntegerField,
)
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
Expand All @@ -38,6 +39,8 @@
IssueLink,
IssueAttachment,
IssueRelation,
IssueAssignee,
User,
)
from plane.app.serializers import (
IssueActivitySerializer,
Expand Down Expand Up @@ -212,11 +215,11 @@ def dashboard_assigned_issues(self, request, slug):
if issue_type == "overdue":
overdue_issues_count = assigned_issues.filter(
state__group__in=["backlog", "unstarted", "started"],
target_date__lt=timezone.now()
target_date__lt=timezone.now(),
).count()
overdue_issues = assigned_issues.filter(
state__group__in=["backlog", "unstarted", "started"],
target_date__lt=timezone.now()
target_date__lt=timezone.now(),
)[:5]
return Response(
{
Expand All @@ -231,11 +234,11 @@ def dashboard_assigned_issues(self, request, slug):
if issue_type == "upcoming":
upcoming_issues_count = assigned_issues.filter(
state__group__in=["backlog", "unstarted", "started"],
target_date__gte=timezone.now()
target_date__gte=timezone.now(),
).count()
upcoming_issues = assigned_issues.filter(
state__group__in=["backlog", "unstarted", "started"],
target_date__gte=timezone.now()
target_date__gte=timezone.now(),
)[:5]
return Response(
{
Expand Down Expand Up @@ -365,11 +368,11 @@ def dashboard_created_issues(self, request, slug):
if issue_type == "overdue":
overdue_issues_count = created_issues.filter(
state__group__in=["backlog", "unstarted", "started"],
target_date__lt=timezone.now()
target_date__lt=timezone.now(),
).count()
overdue_issues = created_issues.filter(
state__group__in=["backlog", "unstarted", "started"],
target_date__lt=timezone.now()
target_date__lt=timezone.now(),
)[:5]
return Response(
{
Expand All @@ -382,11 +385,11 @@ def dashboard_created_issues(self, request, slug):
if issue_type == "upcoming":
upcoming_issues_count = created_issues.filter(
state__group__in=["backlog", "unstarted", "started"],
target_date__gte=timezone.now()
target_date__gte=timezone.now(),
).count()
upcoming_issues = created_issues.filter(
state__group__in=["backlog", "unstarted", "started"],
target_date__gte=timezone.now()
target_date__gte=timezone.now(),
)[:5]
return Response(
{
Expand Down Expand Up @@ -503,7 +506,9 @@ def dashboard_recent_projects(self, request, slug):
).exclude(id__in=unique_project_ids)

# Append additional project IDs to the existing list
unique_project_ids.update(additional_projects.values_list("id", flat=True))
unique_project_ids.update(
additional_projects.values_list("id", flat=True)
)

return Response(
list(unique_project_ids)[:4],
Expand All @@ -512,90 +517,97 @@ def dashboard_recent_projects(self, request, slug):


def dashboard_recent_collaborators(self, request, slug):
# Fetch all project IDs where the user belongs to
user_projects = Project.objects.filter(
project_projectmember__member=request.user,
project_projectmember__is_active=True,
workspace__slug=slug,
).values_list("id", flat=True)

# Fetch all users who have performed an activity in the projects where the user exists
users_with_activities = (
# Subquery to count activities for each project member
activity_count_subquery = (
IssueActivity.objects.filter(
workspace__slug=slug,
project_id__in=user_projects,
actor=OuterRef("member"),
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
)
.values("actor")
.exclude(actor=request.user)
.annotate(num_activities=Count("actor"))
.order_by("-num_activities")
)[:7]

# Get the count of active issues for each user in users_with_activities
users_with_active_issues = []
for user_activity in users_with_activities:
user_id = user_activity["actor"]
active_issue_count = Issue.objects.filter(
assignees__in=[user_id],
state__group__in=["unstarted", "started"],
).count()
users_with_active_issues.append(
{"user_id": user_id, "active_issue_count": active_issue_count}
.annotate(num_activities=Count("pk"))
.values("num_activities")
)

# Get all project members and annotate them with activity counts
project_members_with_activities = (
ProjectMember.objects.filter(
workspace__slug=slug,
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
)
.annotate(
num_activities=Coalesce(
Subquery(activity_count_subquery),
Value(0),
output_field=IntegerField(),
),
is_current_user=Case(
When(member=request.user, then=Value(0)),
default=Value(1),
output_field=IntegerField(),
),
)
.values_list("member", flat=True)
.order_by("is_current_user", "-num_activities")
.distinct()
)
search = request.query_params.get("search", None)
if search:
project_members_with_activities = (
project_members_with_activities.filter(
Q(member__display_name__icontains=search)
| Q(member__first_name__icontains=search)
| Q(member__last_name__icontains=search)
)
)

# Insert the logged-in user's ID and their active issue count at the beginning
active_issue_count = Issue.objects.filter(
assignees__in=[request.user],
state__group__in=["unstarted", "started"],
).count()
return self.paginate(
request=request,
queryset=project_members_with_activities,
controller=self.get_results_controller,
)

if users_with_activities.count() < 7:
# Calculate the additional collaborators needed
additional_collaborators_needed = 7 - users_with_activities.count()

# Fetch additional collaborators from the project_member table
additional_collaborators = list(
set(
ProjectMember.objects.filter(
~Q(member=request.user),
project_id__in=user_projects,
workspace__slug=slug,
)
.exclude(
member__in=[
user["actor"] for user in users_with_activities
]

class DashboardEndpoint(BaseAPIView):
def get_results_controller(self, project_members_with_activities):
user_active_issue_counts = (
User.objects.filter(id__in=project_members_with_activities)
.annotate(
active_issue_count=Count(
Case(
When(
issue_assignee__issue__state__group__in=[
"unstarted",
"started",
],
then=1,
),
output_field=IntegerField(),
)
)
.values_list("member", flat=True)
)
.values("active_issue_count", user_id=F("id"))
)
# Create a dictionary to store the active issue counts by user ID
active_issue_counts_dict = {
user["user_id"]: user["active_issue_count"]
for user in user_active_issue_counts
}

additional_collaborators = additional_collaborators[
:additional_collaborators_needed
# Preserve the sequence of project members with activities
paginated_results = [
{
"user_id": member_id,
"active_issue_count": active_issue_counts_dict.get(
member_id, 0
),
}
for member_id in project_members_with_activities
]
return paginated_results

# Append additional collaborators to the list
for collaborator_id in additional_collaborators:
active_issue_count = Issue.objects.filter(
assignees__in=[collaborator_id],
state__group__in=["unstarted", "started"],
).count()
users_with_active_issues.append(
{
"user_id": str(collaborator_id),
"active_issue_count": active_issue_count,
}
)

users_with_active_issues.insert(
0,
{"user_id": request.user.id, "active_issue_count": active_issue_count},
)

return Response(users_with_active_issues, status=status.HTTP_200_OK)


class DashboardEndpoint(BaseAPIView):
def create(self, request, slug):
serializer = DashboardSerializer(data=request.data)
if serializer.is_valid():
Expand All @@ -622,7 +634,9 @@ def get(self, request, slug, dashboard_id=None):
dashboard_type = request.GET.get("dashboard_type", None)
if dashboard_type == "home":
dashboard, created = Dashboard.objects.get_or_create(
type_identifier=dashboard_type, owned_by=request.user, is_default=True
type_identifier=dashboard_type,
owned_by=request.user,
is_default=True,
)

if created:
Expand All @@ -639,7 +653,9 @@ def get(self, request, slug, dashboard_id=None):

updated_dashboard_widgets = []
for widget_key in widgets_to_fetch:
widget = Widget.objects.filter(key=widget_key).values_list("id", flat=True)
widget = Widget.objects.filter(
key=widget_key
).values_list("id", flat=True)
if widget:
updated_dashboard_widgets.append(
DashboardWidget(
Expand Down
63 changes: 63 additions & 0 deletions apiserver/plane/app/views/workspace.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# Python imports
import jwt
import csv
import io
from datetime import date, datetime
from dateutil.relativedelta import relativedelta

# Django imports
from django.http import HttpResponse
from django.db import IntegrityError
from django.conf import settings
from django.utils import timezone
Expand Down Expand Up @@ -1238,6 +1241,66 @@ def get(self, request, slug, user_id):
)


class ExportWorkspaceUserActivityEndpoint(BaseAPIView):
permission_classes = [
WorkspaceEntityPermission,
NarayanBavisetti marked this conversation as resolved.
Show resolved Hide resolved
]

def generate_csv_from_rows(self, rows):
"""Generate CSV buffer from rows."""
csv_buffer = io.StringIO()
writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL)
[writer.writerow(row) for row in rows]
csv_buffer.seek(0)
return csv_buffer

def get(self, request, slug, user_id):

if not request.GET.get("date"):
return Response(
{"error": "Date is required"},
status=status.HTTP_400_BAD_REQUEST,
)

user_activities = IssueActivity.objects.filter(
~Q(field__in=["comment", "vote", "reaction", "draft"]),
workspace__slug=slug,
created_at__date=request.GET.get("date"),
project__project_projectmember__member=request.user,
actor_id=user_id,
).select_related("actor", "workspace", "issue", "project")[:10000]

header = [
"Actor name",
"Issue ID",
"Project",
"Created at",
"Updated at",
"Action",
"Field",
"Old value",
"New value",
]
rows = [
(
activity.actor.display_name,
f"{activity.project.identifier} - {activity.issue.sequence_id if activity.issue else ''}",
activity.project.name,
activity.created_at,
activity.updated_at,
activity.verb,
activity.field,
activity.old_value,
activity.new_value,
)
for activity in user_activities
]
csv_buffer = self.generate_csv_from_rows([header] + rows)
response = HttpResponse(csv_buffer.getvalue(), content_type="text/csv")
response["Content-Disposition"] = 'attachment; filename="workspace-user-activity.csv"'
return response


class WorkspaceUserProfileEndpoint(BaseAPIView):
def get(self, request, slug, user_id):
user_data = User.objects.get(pk=user_id)
Expand Down
Loading
Loading