From b2451ac03db46f7e011c6fc637b4a7ceaa12efca Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Wed, 21 Jun 2023 12:04:47 +0530 Subject: [PATCH 01/17] feat: added new issue subscriber table --- apiserver/plane/api/serializers/__init__.py | 1 + apiserver/plane/api/serializers/issue.py | 12 +++++++++ apiserver/plane/api/urls.py | 14 +++++++++++ apiserver/plane/api/views/__init__.py | 1 + apiserver/plane/api/views/issue.py | 28 +++++++++++++++++++++ apiserver/plane/db/models/__init__.py | 1 + apiserver/plane/db/models/issue.py | 21 ++++++++++++++++ 7 files changed, 78 insertions(+) diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py index 2b72c5ae1..085bb9bd1 100644 --- a/apiserver/plane/api/serializers/__init__.py +++ b/apiserver/plane/api/serializers/__init__.py @@ -41,6 +41,7 @@ from .issue import ( IssueLinkSerializer, IssueLiteSerializer, IssueAttachmentSerializer, + IssueSubscriberSerializer, ) from .module import ( diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 14782dbe5..540ea9097 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -19,6 +19,7 @@ from plane.db.models import ( IssueProperty, IssueBlocker, IssueAssignee, + IssueSubscriber, IssueLabel, Label, IssueBlocker, @@ -530,3 +531,14 @@ class IssueLiteSerializer(BaseSerializer): "created_at", "updated_at", ] + + +class IssueSubscriberSerializer(BaseSerializer): + project = serializers.PrimaryKeyRelatedField(read_only=True) + workspace = serializers.PrimaryKeyRelatedField(read_only=True) + issue = serializers.PrimaryKeyRelatedField(read_only=True) + subscriber = serializers.PrimaryKeyRelatedField(read_only=True) + + class Meta: + model = IssueSubscriber + fields = "__all__" diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 936fd73ab..bf370063a 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -76,6 +76,7 @@ from plane.api.views import ( IssueLinkViewSet, BulkCreateIssueLabelsEndpoint, IssueAttachmentEndpoint, + IssueSubscriberViewSet, ## End Issues # States StateViewSet, @@ -798,6 +799,19 @@ urlpatterns = [ name="project-issue-comment", ), ## End IssueComments + # Issue Subscribers + path( + "workspaces//projects//issues//subscribers/", + IssueSubscriberViewSet.as_view( + { + "get": "list", + "post": "create", + "delete": "destroy" + } + ), + name="project-issue-subscriber", + ), + ## End Issue Subscribers ## IssueProperty path( "workspaces//projects//issue-properties/", diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index f8d170532..a3c166e80 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -65,6 +65,7 @@ from .issue import ( IssueLinkViewSet, BulkCreateIssueLabelsEndpoint, IssueAttachmentEndpoint, + IssueSubscriberViewSet, ) from .auth_extended import ( diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index cd9f65e48..85d205fed 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -42,6 +42,7 @@ from plane.api.serializers import ( IssueLinkSerializer, IssueLiteSerializer, IssueAttachmentSerializer, + IssueSubscriberSerializer, ) from plane.api.permissions import ( ProjectEntityPermission, @@ -58,6 +59,7 @@ from plane.db.models import ( IssueLink, IssueAttachment, State, + IssueSubscriber, ) from plane.bgtasks.issue_activites_task import issue_activity from plane.utils.grouper import group_results @@ -842,3 +844,29 @@ class IssueAttachmentEndpoint(BaseAPIView): {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) + + +class IssueSubscriberViewSet(BaseViewSet): + serializer_class = IssueSubscriberSerializer + model = IssueSubscriber + + permission_classes = [ + ProjectEntityPermission, + ] + + def perform_create(self, serializer): + serializer.save( + subscriber_id=self.request.user.id, issue_id=self.kwargs.get("issue_id") + ) + + def get_queryset(self): + return ( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(issue_id=self.kwargs.get("issue_id")) + .filter(project__project_projectmember__member=self.request.user) + .order_by("-created_at") + .distinct() + ) diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index 96c649a83..47585207c 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -33,6 +33,7 @@ from .issue import ( IssueLink, IssueSequence, IssueAttachment, + IssueSubscriber, ) from .asset import FileAsset diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index 7efe86d46..ab6c65840 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -399,6 +399,27 @@ class IssueSequence(ProjectBaseModel): ordering = ("-created_at",) +class IssueSubscriber(ProjectBaseModel): + issue = models.ForeignKey( + Issue, on_delete=models.CASCADE, related_name="issue_subscribers" + ) + subscriber = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="issue_subscribers", + ) + + class Meta: + unique_together = ["issue", "subscriber"] + verbose_name = "Issue Subscriber" + verbose_name_plural = "Issue Subscribers" + db_table = "issue_subscribers" + ordering = ("-created_at",) + + def __str__(self): + return f"{self.issue.name} {self.subscriber.email}" + + # TODO: Find a better method to save the model @receiver(post_save, sender=Issue) def create_issue_sequence(sender, instance, created, **kwargs): From f80f98ddef4d48770b86d59cb5a7f3620d52cafd Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 22 Jun 2023 13:38:45 +0530 Subject: [PATCH 02/17] dev: notification model --- apiserver/plane/db/models/notification.py | 33 +++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 apiserver/plane/db/models/notification.py diff --git a/apiserver/plane/db/models/notification.py b/apiserver/plane/db/models/notification.py new file mode 100644 index 000000000..312f2933f --- /dev/null +++ b/apiserver/plane/db/models/notification.py @@ -0,0 +1,33 @@ +# Django imports +from django.db import models + +# Third party imports +from .base import BaseModel + + +class Notification(BaseModel): + workspace = models.ForeignKey( + "db.Workspace", related_name="notifications", on_delete=models.CASCADE + ) + project = models.ForeignKey( + "db.Project", related_name="notifications", on_delete=models.CASCADE, null=True + ) + entity_identifier = models.UUIDField(null=True) + title = models.TextField() + message = models.JSONField(null=True) + message_html = models.TextField(blank=True, default="

") + message_stripped = models.TextField(blank=True, null=True) + sender = models.ForeignKey("db.User", related_name="sent_notifications", on_delete=models.SET_NULL, null=True) + receiver = models.ForeignKey("db.User", related_name="received_notifications", on_delete=models.CASCADE) + read_at = models.DateTimeField(null=True) + snoozed_till = models.DateTimeField(null=True) + + class Meta: + verbose_name = "Notification" + verbose_name_plural = "Notifications" + db_table = "notifications" + ordering = ("-created_at",) + + def __str__(self): + """Return name of the notifications""" + return f"{self.receiver.name} <{self.workspace.name}>" From b22e0625768f0b096b5898936ace76d6882b0736 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Thu, 22 Jun 2023 18:20:59 +0530 Subject: [PATCH 03/17] feat: added CRUD operation for issue subscriber --- apiserver/plane/api/serializers/issue.py | 10 +++++----- apiserver/plane/api/urls.py | 20 +++++++++++++++++++- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 540ea9097..7376cf0ff 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -534,11 +534,11 @@ class IssueLiteSerializer(BaseSerializer): class IssueSubscriberSerializer(BaseSerializer): - project = serializers.PrimaryKeyRelatedField(read_only=True) - workspace = serializers.PrimaryKeyRelatedField(read_only=True) - issue = serializers.PrimaryKeyRelatedField(read_only=True) - subscriber = serializers.PrimaryKeyRelatedField(read_only=True) - class Meta: model = IssueSubscriber fields = "__all__" + read_only_fields = [ + "workspace", + "project", + "issue", + ] diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index bf370063a..9edd92867 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -801,16 +801,34 @@ urlpatterns = [ ## End IssueComments # Issue Subscribers path( - "workspaces//projects//issues//subscribers/", + "workspaces//projects//issues//issue-subscribers/", IssueSubscriberViewSet.as_view( { "get": "list", "post": "create", + } + ), + name="project-issue-subscriber", + ), + path( + "workspaces//projects//issues//issue-subscribers/", + IssueSubscriberViewSet.as_view( + { "delete": "destroy" } ), name="project-issue-subscriber", ), + path( + "workspaces//projects//issues//subscribe/", + IssueSubscriberViewSet.as_view( + { + "get": "subscription_status", + "post": "subscribe", + } + ), + name="project-issue-subscriber", + ), ## End Issue Subscribers ## IssueProperty path( From 9c0a2b74b3d9ba878b5a7eb8b293fa4355a24e66 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Thu, 22 Jun 2023 18:24:22 +0530 Subject: [PATCH 04/17] Revert "feat: added CRUD operation for issue subscriber" This reverts commit b22e0625768f0b096b5898936ace76d6882b0736. --- apiserver/plane/api/serializers/issue.py | 10 +++++----- apiserver/plane/api/urls.py | 20 +------------------- 2 files changed, 6 insertions(+), 24 deletions(-) diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 7376cf0ff..540ea9097 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -534,11 +534,11 @@ class IssueLiteSerializer(BaseSerializer): class IssueSubscriberSerializer(BaseSerializer): + project = serializers.PrimaryKeyRelatedField(read_only=True) + workspace = serializers.PrimaryKeyRelatedField(read_only=True) + issue = serializers.PrimaryKeyRelatedField(read_only=True) + subscriber = serializers.PrimaryKeyRelatedField(read_only=True) + class Meta: model = IssueSubscriber fields = "__all__" - read_only_fields = [ - "workspace", - "project", - "issue", - ] diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 9edd92867..bf370063a 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -801,34 +801,16 @@ urlpatterns = [ ## End IssueComments # Issue Subscribers path( - "workspaces//projects//issues//issue-subscribers/", + "workspaces//projects//issues//subscribers/", IssueSubscriberViewSet.as_view( { "get": "list", "post": "create", - } - ), - name="project-issue-subscriber", - ), - path( - "workspaces//projects//issues//issue-subscribers/", - IssueSubscriberViewSet.as_view( - { "delete": "destroy" } ), name="project-issue-subscriber", ), - path( - "workspaces//projects//issues//subscribe/", - IssueSubscriberViewSet.as_view( - { - "get": "subscription_status", - "post": "subscribe", - } - ), - name="project-issue-subscriber", - ), ## End Issue Subscribers ## IssueProperty path( From cf4bb02a9f5d0942753a8fa7ed18d311c12b4ebf Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Thu, 22 Jun 2023 18:27:40 +0530 Subject: [PATCH 05/17] feat: added CRUD operation for issue subscriber --- apiserver/plane/api/serializers/issue.py | 10 +-- apiserver/plane/api/urls.py | 20 +++++- apiserver/plane/api/views/issue.py | 86 +++++++++++++++++++++++- 3 files changed, 109 insertions(+), 7 deletions(-) diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 540ea9097..7376cf0ff 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -534,11 +534,11 @@ class IssueLiteSerializer(BaseSerializer): class IssueSubscriberSerializer(BaseSerializer): - project = serializers.PrimaryKeyRelatedField(read_only=True) - workspace = serializers.PrimaryKeyRelatedField(read_only=True) - issue = serializers.PrimaryKeyRelatedField(read_only=True) - subscriber = serializers.PrimaryKeyRelatedField(read_only=True) - class Meta: model = IssueSubscriber fields = "__all__" + read_only_fields = [ + "workspace", + "project", + "issue", + ] diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index bf370063a..9edd92867 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -801,16 +801,34 @@ urlpatterns = [ ## End IssueComments # Issue Subscribers path( - "workspaces//projects//issues//subscribers/", + "workspaces//projects//issues//issue-subscribers/", IssueSubscriberViewSet.as_view( { "get": "list", "post": "create", + } + ), + name="project-issue-subscriber", + ), + path( + "workspaces//projects//issues//issue-subscribers/", + IssueSubscriberViewSet.as_view( + { "delete": "destroy" } ), name="project-issue-subscriber", ), + path( + "workspaces//projects//issues//subscribe/", + IssueSubscriberViewSet.as_view( + { + "get": "subscription_status", + "post": "subscribe", + } + ), + name="project-issue-subscriber", + ), ## End Issue Subscribers ## IssueProperty path( diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 85d205fed..7b64a211c 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -856,7 +856,8 @@ class IssueSubscriberViewSet(BaseViewSet): def perform_create(self, serializer): serializer.save( - subscriber_id=self.request.user.id, issue_id=self.kwargs.get("issue_id") + project_id=self.kwargs.get("project_id"), + issue_id=self.kwargs.get("issue_id"), ) def get_queryset(self): @@ -870,3 +871,86 @@ class IssueSubscriberViewSet(BaseViewSet): .order_by("-created_at") .distinct() ) + + def destroy(self, request, slug, project_id, issue_id, subscriber_id): + try: + issue_subscriber = IssueSubscriber.objects.get( + project=project_id, + subscriber=subscriber_id, + workspace__slug=slug, + issue=issue_id, + ) + + issue_subscriber.delete() + + return Response( + {"message": "Removed Subscription"}, + status=status.HTTP_200_OK, + ) + + except IssueSubscriber.DoesNotExist: + return Response( + {"error": "User is not subscribed to this issue"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + def subscribe(self, request, slug, project_id, issue_id): + try: + if IssueSubscriber.objects.filter( + issue_id=issue_id, + subscriber=request.user, + workspace__slug=slug, + project=project_id, + ).exists(): + return Response( + {"message": "User already subscribed to the issue."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + issue = Issue.objects.get(id=issue_id) + + subscriber = IssueSubscriber.objects.create( + issue_id=issue_id, + subscriber_id=request.user.id, + project_id=project_id, + ) + + serilaizer = IssueSubscriberSerializer(subscriber) + return Response(serilaizer.data, status=status.HTTP_201_CREATED) + + except Issue.DoesNotExist: + return Response( + {"error": "Issue does not exists"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong, please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + def subscription_status(self, request, slug, project_id, issue_id): + try: + issue_subscriber = IssueSubscriber.objects.filter( + issue=issue_id, + subscriber=request.user, + workspace__slug=slug, + project=project_id, + ).exists() + return Response({"subscribed": issue_subscriber}, 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, + ) From 0af49306a9edc709e67726658ca4a8d57c453d35 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 22 Jun 2023 18:38:51 +0530 Subject: [PATCH 06/17] dev: notification models and operations --- apiserver/plane/api/serializers/__init__.py | 3 + .../plane/api/serializers/notification.py | 10 +++ apiserver/plane/api/urls.py | 31 +++++++-- apiserver/plane/api/views/__init__.py | 3 + apiserver/plane/api/views/notification.py | 24 +++++++ .../plane/bgtasks/issue_activites_task.py | 65 +++++++++++++++---- apiserver/plane/db/models/__init__.py | 3 + apiserver/plane/db/models/notification.py | 5 +- 8 files changed, 126 insertions(+), 18 deletions(-) create mode 100644 apiserver/plane/api/serializers/notification.py create mode 100644 apiserver/plane/api/views/notification.py diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py index 085bb9bd1..381891f2f 100644 --- a/apiserver/plane/api/serializers/__init__.py +++ b/apiserver/plane/api/serializers/__init__.py @@ -75,4 +75,7 @@ from .estimate import ( ) from .inbox import InboxSerializer, InboxIssueSerializer, IssueStateInboxSerializer + from .analytic import AnalyticViewSerializer + +from .notification import NotificationSerializer diff --git a/apiserver/plane/api/serializers/notification.py b/apiserver/plane/api/serializers/notification.py new file mode 100644 index 000000000..529cb9f9c --- /dev/null +++ b/apiserver/plane/api/serializers/notification.py @@ -0,0 +1,10 @@ +# Module imports +from .base import BaseSerializer +from plane.db.models import Notification + +class NotificationSerializer(BaseSerializer): + + class Meta: + model = Notification + fields = "__all__" + diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index bf370063a..f871f6f3d 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -149,6 +149,9 @@ from plane.api.views import ( ExportAnalyticsEndpoint, DefaultAnalyticsEndpoint, ## End Analytics + # Notification + NotificationViewSet, + ## End Notification ) @@ -803,11 +806,7 @@ urlpatterns = [ path( "workspaces//projects//issues//subscribers/", IssueSubscriberViewSet.as_view( - { - "get": "list", - "post": "create", - "delete": "destroy" - } + {"get": "list", "post": "create", "delete": "destroy"} ), name="project-issue-subscriber", ), @@ -1288,4 +1287,26 @@ urlpatterns = [ name="default-analytics", ), ## End Analytics + # Notification + path( + "workspaces//users/notifications/", + NotificationViewSet.as_view( + { + "get": "list", + } + ), + name="notifications", + ), + path( + "workspaces//users/notifications//", + NotificationViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="notifications", + ), + ## End Notification ] diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index a3c166e80..327dd6037 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -134,6 +134,7 @@ from .estimate import ( from .release import ReleaseNotesEndpoint from .inbox import InboxViewSet, InboxIssueViewSet + from .analytic import ( AnalyticsEndpoint, AnalyticViewViewset, @@ -141,3 +142,5 @@ from .analytic import ( ExportAnalyticsEndpoint, DefaultAnalyticsEndpoint, ) + +from .notification import NotificationViewSet \ No newline at end of file diff --git a/apiserver/plane/api/views/notification.py b/apiserver/plane/api/views/notification.py new file mode 100644 index 000000000..add3c6994 --- /dev/null +++ b/apiserver/plane/api/views/notification.py @@ -0,0 +1,24 @@ +# Third party imports +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from .base import BaseViewSet +from plane.db.models import Notification +from plane.api.serializers import NotificationSerializer + + +class NotificationViewSet(BaseViewSet): + model = Notification + serializer_class = NotificationSerializer + + def get_queryset(self): + return ( + super() + .get_queryset() + .filter( + workspace__slug=self.kwargs.get("slug"), + ) + .select_related("workspace") + ) + diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index b1096e30b..9fa0d2fac 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -20,8 +20,10 @@ from plane.db.models import ( State, Cycle, Module, + IssueSubscriber, + Notification, ) -from plane.api.serializers import IssueActivitySerializer +from plane.api.serializers import IssueActivitySerializer, IssueFlatSerializer # Track Chnages in name @@ -992,18 +994,57 @@ def issue_activity( # Post the updates to segway for integrations and webhooks if len(issue_activities_created): # Don't send activities if the actor is a bot - if settings.PROXY_BASE_URL: - for issue_activity in issue_activities_created: - headers = {"Content-Type": "application/json"} - issue_activity_json = json.dumps( - IssueActivitySerializer(issue_activity).data, - cls=DjangoJSONEncoder, - ) - _ = requests.post( - f"{settings.PROXY_BASE_URL}/hooks/workspaces/{str(issue_activity.workspace_id)}/projects/{str(issue_activity.project_id)}/issues/{str(issue_activity.issue_id)}/issue-activity-hooks/", - json=issue_activity_json, - headers=headers, + try: + if settings.PROXY_BASE_URL: + for issue_activity in issue_activities_created: + headers = {"Content-Type": "application/json"} + issue_activity_json = json.dumps( + IssueActivitySerializer(issue_activity).data, + cls=DjangoJSONEncoder, + ) + _ = requests.post( + f"{settings.PROXY_BASE_URL}/hooks/workspaces/{str(issue_activity.workspace_id)}/projects/{str(issue_activity.project_id)}/issues/{str(issue_activity.issue_id)}/issue-activity-hooks/", + json=issue_activity_json, + headers=headers, + ) + except Exception as e: + capture_exception(e) + + # Create Notifications + bulk_notifications = [] + + issue_subscribers = ( + IssueSubscriber.objects.filter(project=project) + .exclude(subscriber_id=actor_id) + .values_list("subscriber") + ) + + issue = Issue.objects.get(project=project, pk=issue_id) + for subscriber in issue_subscribers: + for issue_activity in issue_activities_created: + bulk_notifications.append( + Notification( + workspace=project.workspace, + sender="in_app:issue_activities", + triggered_by=actor, + receiver_id=subscriber, + entity_identifier=issue_id, + entity_name="issue", + project=project, + title=issue_activity.comment, + data={ + "issue": { + "id": str(issue_id), + "identifier": str(project.identifier), + "sequence_id": issue.sequence_id, + "state_name": issue.state.name, + "state_group": issue.state.group, + }, + "issue_activity": str(issue_activity.id), + }, ) + ) + return except Exception as e: capture_exception(e) diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index 47585207c..1c075478d 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -67,4 +67,7 @@ from .page import Page, PageBlock, PageFavorite, PageLabel from .estimate import Estimate, EstimatePoint from .inbox import Inbox, InboxIssue + from .analytic import AnalyticView + +from .notification import Notification \ No newline at end of file diff --git a/apiserver/plane/db/models/notification.py b/apiserver/plane/db/models/notification.py index 312f2933f..10ee1e709 100644 --- a/apiserver/plane/db/models/notification.py +++ b/apiserver/plane/db/models/notification.py @@ -12,12 +12,15 @@ class Notification(BaseModel): project = models.ForeignKey( "db.Project", related_name="notifications", on_delete=models.CASCADE, null=True ) + data = models.JSONField(null=True) entity_identifier = models.UUIDField(null=True) + entity_name = models.CharField(max_length=255) title = models.TextField() message = models.JSONField(null=True) message_html = models.TextField(blank=True, default="

") message_stripped = models.TextField(blank=True, null=True) - sender = models.ForeignKey("db.User", related_name="sent_notifications", on_delete=models.SET_NULL, null=True) + sender = models.CharField(max_length=255) + triggered_by = models.ForeignKey("db.User", related_name="triggered_notifications", on_delete=models.SET_NULL, null=True) receiver = models.ForeignKey("db.User", related_name="received_notifications", on_delete=models.CASCADE) read_at = models.DateTimeField(null=True) snoozed_till = models.DateTimeField(null=True) From 2cd3dc2f9db7b926ae9dc19e90ccd02ba15d42db Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 22 Jun 2023 19:01:15 +0530 Subject: [PATCH 07/17] dev: remove delete endpoint response data --- apiserver/plane/api/views/issue.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index d41d1574d..5a08b0f0c 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -925,8 +925,7 @@ class IssueSubscriberViewSet(BaseViewSet): ) issue_subscriber.delete() return Response( - {"message": "Removed Subscription"}, - status=status.HTTP_200_OK, + status=status.HTTP_204_NO_CONTENT, ) except IssueSubscriber.DoesNotExist: return Response( From dcde6b2567eeeef652c594e0b8e2476785fca394 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 22 Jun 2023 21:31:30 +0530 Subject: [PATCH 08/17] dev: notification endpoints and fix bg worker for saving notifications --- apiserver/plane/api/views/notification.py | 29 +++++++++++++++++++ .../plane/bgtasks/issue_activites_task.py | 9 ++++-- apiserver/plane/db/models/notification.py | 2 +- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/apiserver/plane/api/views/notification.py b/apiserver/plane/api/views/notification.py index add3c6994..1ccd43212 100644 --- a/apiserver/plane/api/views/notification.py +++ b/apiserver/plane/api/views/notification.py @@ -1,6 +1,7 @@ # Third party imports from rest_framework import status from rest_framework.response import Response +from sentry_sdk import capture_exception # Module imports from .base import BaseViewSet @@ -18,7 +19,35 @@ class NotificationViewSet(BaseViewSet): .get_queryset() .filter( workspace__slug=self.kwargs.get("slug"), + receiver_id=self.request.user.id, ) .select_related("workspace") ) + def partial_update(self, request, slug, pk): + try: + notification = Notification.objects.get( + workspace__slug=slug, pk=pk, receiver=request.user + ) + # Only read_at and snoozed_till can be updated + notification_data = { + "read_at": request.data.get("read_at", None), + "snoozed_till": request.data.get("snoozed_till", None), + } + serializer = NotificationSerializer(notification, data=notification_data, partial=True) + + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except Notification.DoesNotExist: + return Response( + {"error": "Notification does not exists"}, + status=status.HTTP_400_BAD_REQUEST, + ) + 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/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index 9e4f9475f..f14d8ddc8 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -1014,9 +1014,9 @@ def issue_activity( bulk_notifications = [] issue_subscribers = ( - IssueSubscriber.objects.filter(project=project) + IssueSubscriber.objects.filter(project=project, issue_id=issue_id) .exclude(subscriber_id=actor_id) - .values_list("subscriber") + .values_list("subscriber", flat=True) ) issue = Issue.objects.get(project=project, pk=issue_id) @@ -1026,7 +1026,7 @@ def issue_activity( Notification( workspace=project.workspace, sender="in_app:issue_activities", - triggered_by=actor, + triggered_by_id=actor_id, receiver_id=subscriber, entity_identifier=issue_id, entity_name="issue", @@ -1045,6 +1045,9 @@ def issue_activity( ) ) + # Bulk create notifications + Notification.objects.bulk_create(bulk_notifications, batch_size=100) + return except Exception as e: # Print logs if in DEBUG mode diff --git a/apiserver/plane/db/models/notification.py b/apiserver/plane/db/models/notification.py index 10ee1e709..6c4777bcd 100644 --- a/apiserver/plane/db/models/notification.py +++ b/apiserver/plane/db/models/notification.py @@ -33,4 +33,4 @@ class Notification(BaseModel): def __str__(self): """Return name of the notifications""" - return f"{self.receiver.name} <{self.workspace.name}>" + return f"{self.receiver.email} <{self.workspace.name}>" From be96ec47ba1160c618d4a10b15555ab30040371b Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Fri, 23 Jun 2023 01:15:35 +0530 Subject: [PATCH 09/17] feat: added list and unsubscribe function in issue subscriber --- apiserver/plane/api/serializers/project.py | 1 + apiserver/plane/api/urls.py | 1 + apiserver/plane/api/views/issue.py | 51 ++++++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/apiserver/plane/api/serializers/project.py b/apiserver/plane/api/serializers/project.py index 18ee19e7b..cbea7171a 100644 --- a/apiserver/plane/api/serializers/project.py +++ b/apiserver/plane/api/serializers/project.py @@ -96,6 +96,7 @@ class ProjectMemberSerializer(BaseSerializer): workspace = WorkSpaceSerializer(read_only=True) project = ProjectSerializer(read_only=True) member = UserLiteSerializer(read_only=True) + is_subscribed = serializers.BooleanField(read_only=True) class Meta: model = ProjectMember diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 0e084faea..a75a878de 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -824,6 +824,7 @@ urlpatterns = [ { "get": "subscription_status", "post": "subscribe", + "delete": "unsubscribe", } ), name="project-issue-subscribers", diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 81f7889b5..935377d46 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -15,6 +15,7 @@ from django.db.models import ( Value, CharField, When, + Exists, ) from django.core.serializers.json import DjangoJSONEncoder from django.utils.decorators import method_decorator @@ -43,6 +44,7 @@ from plane.api.serializers import ( IssueLiteSerializer, IssueAttachmentSerializer, IssueSubscriberSerializer, + ProjectMemberSerializer, ) from plane.api.permissions import ( ProjectEntityPermission, @@ -60,6 +62,7 @@ from plane.db.models import ( IssueAttachment, State, IssueSubscriber, + ProjectMember, ) from plane.bgtasks.issue_activites_task import issue_activity from plane.utils.grouper import group_results @@ -868,6 +871,30 @@ class IssueSubscriberViewSet(BaseViewSet): ProjectEntityPermission, ] + def list(self, request, slug, project_id, issue_id): + try: + members = ProjectMember.objects.filter( + workspace__slug=slug, project_id=project_id + ) + members = members.annotate( + is_subscribed=Exists( + IssueSubscriber.objects.filter( + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + subscriber=OuterRef("member"), + ) + ) + ) + serializer = ProjectMemberSerializer(members, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + except Exception as e: + capture_exception(e) + return Response( + {"error": e}, + status=status.HTTP_400_BAD_REQUEST, + ) + def perform_create(self, serializer): serializer.save( project_id=self.kwargs.get("project_id"), @@ -913,6 +940,30 @@ class IssueSubscriberViewSet(BaseViewSet): status=status.HTTP_400_BAD_REQUEST, ) + def unsubscribe(self, request, slug, project_id, issue_id): + try: + issue_subscriber = IssueSubscriber.objects.get( + project=project_id, + subscriber=request.user, + workspace__slug=slug, + issue=issue_id, + ) + issue_subscriber.delete() + return Response( + status=status.HTTP_204_NO_CONTENT, + ) + except IssueSubscriber.DoesNotExist: + return Response( + {"error": "User subscribed to this issue"}, + status=status.HTTP_400_BAD_REQUEST, + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + def subscription_status(self, request, slug, project_id, issue_id): try: issue_subscriber = IssueSubscriber.objects.filter( From e73b39fcdfe7cfb7dd76c66d2e678032ac3d18db Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Fri, 23 Jun 2023 11:25:27 +0530 Subject: [PATCH 10/17] dev: filter by snoozed and response update for list and permissions --- apiserver/plane/api/serializers/__init__.py | 1 + apiserver/plane/api/serializers/project.py | 11 +- apiserver/plane/api/views/issue.py | 107 +++++++++++--------- apiserver/plane/api/views/notification.py | 30 +++++- 4 files changed, 100 insertions(+), 49 deletions(-) diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py index 381891f2f..2ff210f98 100644 --- a/apiserver/plane/api/serializers/__init__.py +++ b/apiserver/plane/api/serializers/__init__.py @@ -21,6 +21,7 @@ from .project import ( ProjectIdentifierSerializer, ProjectFavoriteSerializer, ProjectLiteSerializer, + ProjectMemberLiteSerializer, ) from .state import StateSerializer, StateLiteSerializer from .view import IssueViewSerializer, IssueViewFavoriteSerializer diff --git a/apiserver/plane/api/serializers/project.py b/apiserver/plane/api/serializers/project.py index cbea7171a..f96be09ab 100644 --- a/apiserver/plane/api/serializers/project.py +++ b/apiserver/plane/api/serializers/project.py @@ -96,7 +96,6 @@ class ProjectMemberSerializer(BaseSerializer): workspace = WorkSpaceSerializer(read_only=True) project = ProjectSerializer(read_only=True) member = UserLiteSerializer(read_only=True) - is_subscribed = serializers.BooleanField(read_only=True) class Meta: model = ProjectMember @@ -135,3 +134,13 @@ class ProjectLiteSerializer(BaseSerializer): model = Project fields = ["id", "identifier", "name"] read_only_fields = fields + + +class ProjectMemberLiteSerializer(BaseSerializer): + member = UserLiteSerializer(read_only=True) + is_subscribed = serializers.BooleanField(read_only=True) + + class Meta: + model = ProjectMember + fields = ["member", "id", "is_subscribed"] + read_only_fields = fields diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 935377d46..f68219834 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -45,11 +45,13 @@ from plane.api.serializers import ( IssueAttachmentSerializer, IssueSubscriberSerializer, ProjectMemberSerializer, + ProjectMemberLiteSerializer, ) from plane.api.permissions import ( ProjectEntityPermission, WorkSpaceAdminPermission, ProjectMemberPermission, + ProjectLitePermission, ) from plane.db.models import ( Project, @@ -871,29 +873,17 @@ class IssueSubscriberViewSet(BaseViewSet): ProjectEntityPermission, ] - def list(self, request, slug, project_id, issue_id): - try: - members = ProjectMember.objects.filter( - workspace__slug=slug, project_id=project_id - ) - members = members.annotate( - is_subscribed=Exists( - IssueSubscriber.objects.filter( - workspace__slug=slug, - project_id=project_id, - issue_id=issue_id, - subscriber=OuterRef("member"), - ) - ) - ) - serializer = ProjectMemberSerializer(members, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) - return Response( - {"error": e}, - status=status.HTTP_400_BAD_REQUEST, - ) + def get_permissions(self): + if self.action in ["subscribe", "unsubscribe", "subscription_status"]: + self.permission_classes = [ + ProjectLitePermission, + ] + else: + self.permission_classes = [ + ProjectEntityPermission, + ] + + return super(IssueSubscriberViewSet, self).get_permissions() def perform_create(self, serializer): serializer.save( @@ -913,6 +903,53 @@ class IssueSubscriberViewSet(BaseViewSet): .distinct() ) + def list(self, request, slug, project_id, issue_id): + try: + members = ProjectMember.objects.filter( + workspace__slug=slug, project_id=project_id + ).annotate( + is_subscribed=Exists( + IssueSubscriber.objects.filter( + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + subscriber=OuterRef("member"), + ) + ) + ).select_related("member") + serializer = ProjectMemberLiteSerializer(members, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + except Exception as e: + capture_exception(e) + return Response( + {"error": e}, + status=status.HTTP_400_BAD_REQUEST, + ) + + def destroy(self, request, slug, project_id, issue_id, subscriber_id): + try: + issue_subscriber = IssueSubscriber.objects.get( + project=project_id, + subscriber=subscriber_id, + workspace__slug=slug, + issue=issue_id, + ) + issue_subscriber.delete() + return Response( + status=status.HTTP_204_NO_CONTENT, + ) + except IssueSubscriber.DoesNotExist: + return Response( + {"error": "User is not subscribed to this issue"}, + status=status.HTTP_400_BAD_REQUEST, + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + def subscribe(self, request, slug, project_id, issue_id): try: if IssueSubscriber.objects.filter( @@ -979,27 +1016,3 @@ class IssueSubscriberViewSet(BaseViewSet): {"error": "Something went wrong, please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) - - def destroy(self, request, slug, project_id, issue_id, subscriber_id): - try: - issue_subscriber = IssueSubscriber.objects.get( - project=project_id, - subscriber=subscriber_id, - workspace__slug=slug, - issue=issue_id, - ) - issue_subscriber.delete() - return Response( - status=status.HTTP_204_NO_CONTENT, - ) - except IssueSubscriber.DoesNotExist: - return Response( - {"error": "User is not subscribed to this issue"}, - status=status.HTTP_400_BAD_REQUEST, - ) - 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/notification.py b/apiserver/plane/api/views/notification.py index 1ccd43212..6c3f3587b 100644 --- a/apiserver/plane/api/views/notification.py +++ b/apiserver/plane/api/views/notification.py @@ -1,3 +1,7 @@ +# Django imports +from django.db.models import Q +from django.utils import timezone + # Third party imports from rest_framework import status from rest_framework.response import Response @@ -24,6 +28,28 @@ class NotificationViewSet(BaseViewSet): .select_related("workspace") ) + def list(self, request, slug): + try: + order_by = request.GET.get("ordeer_by", "-created_at") + snoozed = request.GET.get("snoozed", "false") + notifications = Notification.objects.filter( + workspace__slug=slug, receiver=request.user + ).order_by(order_by) + + if snoozed == "false": + notifications = notifications.filter( + Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True), + ) + + serializer = NotificationSerializer(notifications, 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, + ) + def partial_update(self, request, slug, pk): try: notification = Notification.objects.get( @@ -34,7 +60,9 @@ class NotificationViewSet(BaseViewSet): "read_at": request.data.get("read_at", None), "snoozed_till": request.data.get("snoozed_till", None), } - serializer = NotificationSerializer(notification, data=notification_data, partial=True) + serializer = NotificationSerializer( + notification, data=notification_data, partial=True + ) if serializer.is_valid(): serializer.save() From ea605cf48c45a03abd37ee32492bab60ad361e51 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 3 Jul 2023 17:28:09 +0530 Subject: [PATCH 11/17] dev: update issue notifications --- apiserver/plane/bgtasks/issue_activites_task.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index f14d8ddc8..78be2ae06 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -22,6 +22,7 @@ from plane.db.models import ( Module, IssueSubscriber, Notification, + IssueAssignee, ) from plane.api.serializers import IssueActivitySerializer, IssueFlatSerializer @@ -960,6 +961,12 @@ def issue_activity( actor = User.objects.get(pk=actor_id) project = Project.objects.get(pk=project_id) + # add the user to issue subscriber + try: + _ = IssueSubscriber.objects.create(issue_id=issue_id, subscriber=actor) + except Exception as e: + pass + ACTIVITY_MAPPER = { "issue.activity.created": create_issue_activity, "issue.activity.updated": update_issue_activity, @@ -1013,12 +1020,20 @@ def issue_activity( # Create Notifications bulk_notifications = [] - issue_subscribers = ( + issue_subscribers = list( IssueSubscriber.objects.filter(project=project, issue_id=issue_id) .exclude(subscriber_id=actor_id) .values_list("subscriber", flat=True) ) + issue_assignees = list( + IssueAssignee.objects.filter(project=project, issue_id=issue_id) + .exclude(assignee_id=actor_id) + .values_list("assignee", flat=True) + ) + + issue_subscribers = issue_subscribers + issue_assignees + issue = Issue.objects.get(project=project, pk=issue_id) for subscriber in issue_subscribers: for issue_activity in issue_activities_created: From 581dff548213d43528dfa96c7c355436f8090af2 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Fri, 7 Jul 2023 16:02:17 +0530 Subject: [PATCH 12/17] dev: notification segregation --- apiserver/plane/api/views/notification.py | 31 +++++++++++++++++-- .../plane/bgtasks/issue_activites_task.py | 3 ++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/apiserver/plane/api/views/notification.py b/apiserver/plane/api/views/notification.py index 6c3f3587b..83758484b 100644 --- a/apiserver/plane/api/views/notification.py +++ b/apiserver/plane/api/views/notification.py @@ -9,7 +9,7 @@ from sentry_sdk import capture_exception # Module imports from .base import BaseViewSet -from plane.db.models import Notification +from plane.db.models import Notification, IssueAssignee, IssueSubscriber, Issue from plane.api.serializers import NotificationSerializer @@ -30,12 +30,37 @@ class NotificationViewSet(BaseViewSet): def list(self, request, slug): try: - order_by = request.GET.get("ordeer_by", "-created_at") + order_by = request.GET.get("order_by", "-created_at") snoozed = request.GET.get("snoozed", "false") + + # Filter type + type = request.GET.get("type", "all") + notifications = Notification.objects.filter( - workspace__slug=slug, receiver=request.user + workspace__slug=slug, receiver_id=request.user.id ).order_by(order_by) + # Subscribed issues + if type == "watching": + issue_ids = IssueSubscriber.objects.filter( + workspace__slug=slug, subsriber_id=request.user.id + ).values_list("issue_id", flat=True) + notifications = notifications.filter(entity_id__in=issue_ids) + + # Assigned Issues + if type == "assigned": + issue_ids = IssueAssignee.objects.filter( + workspace__slug=slug, assignee_id=request.user.id + ).values_list("issue_id", flat=True) + notifications = notifications.filter(entity_id__in=issue_ids) + + # Created issues + if type == "created": + issue_ids = Issue.objects.filter( + workspace__slug=slug, created_by=request.user + ).values_list("pk", flat=True) + notifications = notifications.filter(entity_id__in=issue_ids) + if snoozed == "false": notifications = notifications.filter( Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True), diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index 78be2ae06..d5992256e 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -1034,6 +1034,9 @@ def issue_activity( issue_subscribers = issue_subscribers + issue_assignees + if issue.created_by_id: + issue_subscribers = issue_subscribers + [issue.created_by_id] + issue = Issue.objects.get(project=project, pk=issue_id) for subscriber in issue_subscribers: for issue_activity in issue_activities_created: From 90c819b1ba066b0aef175a7d23107f0972a99053 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Fri, 7 Jul 2023 18:07:06 +0530 Subject: [PATCH 13/17] dev: update notifications --- apiserver/plane/bgtasks/issue_activites_task.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index d5992256e..bf3d0c91c 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -24,7 +24,7 @@ from plane.db.models import ( Notification, IssueAssignee, ) -from plane.api.serializers import IssueActivitySerializer, IssueFlatSerializer +from plane.api.serializers import IssueActivitySerializer # Track Chnages in name From 6c3be6faba9b5dc6aa802a0201756098d4c03d57 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 10 Jul 2023 13:51:36 +0530 Subject: [PATCH 14/17] dev: notification filtering --- apiserver/plane/api/views/notification.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apiserver/plane/api/views/notification.py b/apiserver/plane/api/views/notification.py index 83758484b..318c3b1f7 100644 --- a/apiserver/plane/api/views/notification.py +++ b/apiserver/plane/api/views/notification.py @@ -45,21 +45,21 @@ class NotificationViewSet(BaseViewSet): issue_ids = IssueSubscriber.objects.filter( workspace__slug=slug, subsriber_id=request.user.id ).values_list("issue_id", flat=True) - notifications = notifications.filter(entity_id__in=issue_ids) + notifications = notifications.filter(entity_identifier__in=issue_ids) # Assigned Issues if type == "assigned": issue_ids = IssueAssignee.objects.filter( workspace__slug=slug, assignee_id=request.user.id ).values_list("issue_id", flat=True) - notifications = notifications.filter(entity_id__in=issue_ids) + notifications = notifications.filter(entity_identifier__in=issue_ids) # Created issues if type == "created": issue_ids = Issue.objects.filter( workspace__slug=slug, created_by=request.user ).values_list("pk", flat=True) - notifications = notifications.filter(entity_id__in=issue_ids) + notifications = notifications.filter(entity_identifier__in=issue_ids) if snoozed == "false": notifications = notifications.filter( @@ -69,7 +69,7 @@ class NotificationViewSet(BaseViewSet): serializer = NotificationSerializer(notifications, many=True) return Response(serializer.data, status=status.HTTP_200_OK) except Exception as e: - capture_exception(e) + print(e) return Response( {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, From 624dfdb2b60ab55efb9f4012e5e7668c723fcd20 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 10 Jul 2023 17:52:02 +0530 Subject: [PATCH 15/17] dev: add issue name in notifications --- apiserver/plane/bgtasks/issue_activites_task.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index bf3d0c91c..7bb6010dd 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -1053,6 +1053,7 @@ def issue_activity( data={ "issue": { "id": str(issue_id), + "name": str(issue.name), "identifier": str(project.identifier), "sequence_id": issue.sequence_id, "state_name": issue.state.name, From cc513b9030662d881f3fc768cbdf622ab53a0336 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Tue, 11 Jul 2023 12:53:06 +0530 Subject: [PATCH 16/17] dev: notification new endpoints --- apiserver/plane/api/urls.py | 20 ++++ apiserver/plane/api/views/notification.py | 113 ++++++++++++++++++++-- apiserver/plane/db/models/notification.py | 1 + 3 files changed, 127 insertions(+), 7 deletions(-) diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 7ce79855b..34e711be6 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -1326,5 +1326,25 @@ urlpatterns = [ ), name="notifications", ), + path( + "workspaces//users/notifications//read/", + NotificationViewSet.as_view( + { + "post": "mark_read", + "delete": "mark_unread", + } + ), + name="notifications", + ), + path( + "workspaces//users/notifications//archive/", + NotificationViewSet.as_view( + { + "post": "archive", + "delete": "unarchive", + } + ), + name="notifications", + ), ## End Notification ] diff --git a/apiserver/plane/api/views/notification.py b/apiserver/plane/api/views/notification.py index 318c3b1f7..fa1e280d6 100644 --- a/apiserver/plane/api/views/notification.py +++ b/apiserver/plane/api/views/notification.py @@ -32,6 +32,7 @@ class NotificationViewSet(BaseViewSet): try: order_by = request.GET.get("order_by", "-created_at") snoozed = request.GET.get("snoozed", "false") + archived = request.GET.get("archived", "false") # Filter type type = request.GET.get("type", "all") @@ -40,6 +41,24 @@ class NotificationViewSet(BaseViewSet): workspace__slug=slug, receiver_id=request.user.id ).order_by(order_by) + # Filter for snoozed notifications + if snoozed == "false": + notifications = notifications.filter( + Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True), + ) + + if snoozed == "true": + notifications = notifications.filter( + snoozed_till__lt=timezone.now(), + ) + + # Filter for archived or unarchive + if archived == "true": + notifications = notifications.filter(archived_at__isnull=True) + + if archived == "false": + notifications = notifications.filter(archived_at__isnull=False) + # Subscribed issues if type == "watching": issue_ids = IssueSubscriber.objects.filter( @@ -61,15 +80,10 @@ class NotificationViewSet(BaseViewSet): ).values_list("pk", flat=True) notifications = notifications.filter(entity_identifier__in=issue_ids) - if snoozed == "false": - notifications = notifications.filter( - Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True), - ) - serializer = NotificationSerializer(notifications, many=True) return Response(serializer.data, status=status.HTTP_200_OK) except Exception as e: - print(e) + capture_exception(e) return Response( {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, @@ -82,7 +96,6 @@ class NotificationViewSet(BaseViewSet): ) # Only read_at and snoozed_till can be updated notification_data = { - "read_at": request.data.get("read_at", None), "snoozed_till": request.data.get("snoozed_till", None), } serializer = NotificationSerializer( @@ -104,3 +117,89 @@ class NotificationViewSet(BaseViewSet): {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) + + def mark_read(self, request, slug, pk): + try: + notification = Notification.objects.get( + receiver=request.user, workspace__slug=slug, pk=pk + ) + notification.read_at = timezone.now() + notification.save() + serializer = NotificationSerializer(notification) + return Response(serializer.data, status=status.HTTP_200_OK) + except Notification.DoesNotExist: + return Response( + {"error": "Notification does not exists"}, + status=status.HTTP_400_BAD_REQUEST, + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + def mark_unread(self, request, slug, pk): + try: + notification = Notification.objects.get( + receiver=request.user, workspace__slug=slug, pk=pk + ) + notification.read_at = None + notification.save() + serializer = NotificationSerializer(notification) + return Response(serializer.data, status=status.HTTP_200_OK) + except Notification.DoesNotExist: + return Response( + {"error": "Notification does not exists"}, + status=status.HTTP_400_BAD_REQUEST, + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + + def archive(self, request, slug, pk): + try: + notification = Notification.objects.get( + receiver=request.user, workspace__slug=slug, pk=pk + ) + notification.archived_at = timezone.now() + notification.save() + serializer = NotificationSerializer(notification) + return Response(serializer.data, status=status.HTTP_200_OK) + except Notification.DoesNotExist: + return Response( + {"error": "Notification does not exists"}, + status=status.HTTP_400_BAD_REQUEST, + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + def unarchive(self, request, slug, pk): + try: + notification = Notification.objects.get( + receiver=request.user, workspace__slug=slug, pk=pk + ) + notification.archived_at = None + notification.save() + serializer = NotificationSerializer(notification) + return Response(serializer.data, status=status.HTTP_200_OK) + except Notification.DoesNotExist: + return Response( + {"error": "Notification does not exists"}, + status=status.HTTP_400_BAD_REQUEST, + ) + 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/db/models/notification.py b/apiserver/plane/db/models/notification.py index 6c4777bcd..3df935718 100644 --- a/apiserver/plane/db/models/notification.py +++ b/apiserver/plane/db/models/notification.py @@ -24,6 +24,7 @@ class Notification(BaseModel): receiver = models.ForeignKey("db.User", related_name="received_notifications", on_delete=models.CASCADE) read_at = models.DateTimeField(null=True) snoozed_till = models.DateTimeField(null=True) + archived_at = models.DateTimeField(null=True) class Meta: verbose_name = "Notification" From 53487eaadd9fc2ccc21568dc0e149a85a4da2f46 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Wed, 12 Jul 2023 12:18:33 +0530 Subject: [PATCH 17/17] fix: pushing local settings --- apiserver/plane/settings/local.py | 36 +++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/apiserver/plane/settings/local.py b/apiserver/plane/settings/local.py index e6f5f8e39..194b2629f 100644 --- a/apiserver/plane/settings/local.py +++ b/apiserver/plane/settings/local.py @@ -10,9 +10,7 @@ from sentry_sdk.integrations.redis import RedisIntegration from .common import * # noqa -DEBUG = int(os.environ.get( - "DEBUG", 1 -)) == 1 +DEBUG = int(os.environ.get("DEBUG", 1)) == 1 EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" @@ -27,13 +25,11 @@ DATABASES = { } } -DOCKERIZED = int(os.environ.get( - "DOCKERIZED", 0 -)) == 1 +DOCKERIZED = int(os.environ.get("DOCKERIZED", 0)) == 1 USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1 -FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880)) +FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880)) if DOCKERIZED: DATABASES["default"] = dj_database_url.config() @@ -65,6 +61,27 @@ if os.environ.get("SENTRY_DSN", False): traces_sample_rate=0.7, profiles_sample_rate=1.0, ) +else: + LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "handlers": { + "console": { + "class": "logging.StreamHandler", + }, + }, + "root": { + "handlers": ["console"], + "level": "DEBUG", + }, + "loggers": { + "*": { + "handlers": ["console"], + "level": "DEBUG", + "propagate": True, + }, + }, + } REDIS_HOST = "localhost" REDIS_PORT = 6379 @@ -83,8 +100,9 @@ PROXY_BASE_URL = os.environ.get("PROXY_BASE_URL", False) ANALYTICS_SECRET_KEY = os.environ.get("ANALYTICS_SECRET_KEY", False) ANALYTICS_BASE_API = os.environ.get("ANALYTICS_BASE_API", False) +OPENAI_API_BASE = os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1") OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False) -GPT_ENGINE = os.environ.get("GPT_ENGINE", "text-davinci-003") +GPT_ENGINE = os.environ.get("GPT_ENGINE", "gpt-3.5-turbo") SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False) @@ -95,4 +113,4 @@ CELERY_BROKER_URL = os.environ.get("REDIS_URL") GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False) -ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1" \ No newline at end of file +ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1"