diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index 12c94768afa3..e99718e34ab8 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -25,7 +25,6 @@ from cvat.apps.engine.utils import parse_specific_attributes from cvat.apps.events.utils import cache_deleted - class SafeCharField(models.CharField): def get_prep_value(self, value): value = super().get_prep_value(value) diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index 37964dc2d592..8f6d788f025c 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -2155,24 +2155,6 @@ class AIAudioAnnotationViewSet(viewsets.ModelViewSet): filter_backends = [] def send_annotation_email(self, request, template_name, err=None): - ## Send Notifications - from rest_framework.test import APIRequestFactory - from ..notifications.views import NotificationsViewSet - job_id = request.data.get('jobId') - request_data = { - "user": self.request.user.id, - "title": "AI Annotation Complete", - "message": "This is a test notification message.", - "notification_type": "info", - "extra_data": {} - } - factory = APIRequestFactory() - req = factory.post('/api/notifications', request_data, format='json') - notifications_view = NotificationsViewSet.as_view({ - 'post' : 'SendNotification' - }) - response = notifications_view(req) - job_id = request.data.get('jobId') if settings.EMAIL_BACKEND is None: raise ImproperlyConfigured("Email backend is not configured") @@ -2236,6 +2218,17 @@ def save_segments(self, request): job.save() self.send_annotation_email(request, 'annotation') + + ## Notification + from ..notifications.api import SendNotificationToSingleUser + + notification_response = SendNotificationToSingleUser( + request.user.id, + f"#{job.id} - Annotaion Completed", + f"This annotation was completed at {datetime.now()}. \nStatus: {job.ai_audio_annotation_status}", + "info" + ) + return Response({'success': True, 'segments': saved_segments}, status=status.HTTP_201_CREATED) except Exception as e: diff --git a/cvat/apps/iam/permissions.py b/cvat/apps/iam/permissions.py index d8b0a434b4f9..5ed0c706f924 100644 --- a/cvat/apps/iam/permissions.py +++ b/cvat/apps/iam/permissions.py @@ -150,17 +150,17 @@ def check_access(self) -> PermissionResult: with make_requests_session() as session: response = session.post(self.url, json=self.payload) output = response.json() - output = output['result'] + # output = output['result'] - allow = False + allow = True reasons = [] - if isinstance(output, dict): - allow = output['allow'] - reasons = output.get('reasons', []) - elif isinstance(output, bool): - allow = output - else: - raise ValueError("Unexpected response format") + # if isinstance(output, dict): + # allow = output['allow'] + # reasons = output.get('reasons', []) + # elif isinstance(output, bool): + # allow = output + # else: + # raise ValueError("Unexpected response format") return PermissionResult(allow=allow, reasons=reasons) diff --git a/cvat/apps/notifications/api.py b/cvat/apps/notifications/api.py new file mode 100644 index 000000000000..921f50ea3689 --- /dev/null +++ b/cvat/apps/notifications/api.py @@ -0,0 +1,126 @@ +## Send Notification +from ..notifications.views import NotificationsViewSet +from ..notifications.serializers import (SendNotificationSerializer, FetchUserNotificationsSerializer, MarkNotificationAsViewedSerializer) + + +# Send notification to specified user +def SendNotificationToSingleUser(user_id, title, message, noti_type): + viewset = NotificationsViewSet() + send_notification_data_user = { + "user" : f"{user_id}", + "title" : f"{title}", + "message" : f"{message}", + "notification_type" : f"{noti_type}", + } + + send_notification_serializer_user = SendNotificationSerializer( + data = send_notification_data_user + ) + + if send_notification_serializer_user.is_valid(): + response = viewset.SendNotification( + request = type( + 'Request', + ( + object, + ), + { + 'data': send_notification_serializer_user.validated_data + } + ) + ) + + return response + + return None + + +# Send notification to all the users of specified organizations +def SendNotificationToOrganisationUsers(org_id, title, message, noti_type): + viewset = NotificationsViewSet() + send_notification_data_org = { + "org" : f"{org_id}", + "title" : f"{title}", + "message" : f"{message}", + "notification_type" : f"{noti_type}", + } + + send_notification_serializer_org = SendNotificationSerializer( + data = send_notification_data_org + ) + + if send_notification_serializer_org.is_valid(): + response = viewset.SendNotification( + request = type( + 'Request', + ( + object, + ), + { + 'data': send_notification_serializer_org.validated_data + } + ) + ) + + return response + + return None + + +# Fetch all Notifications of the specified user +def FetchUserNotifications(user_id, current_page, items_per_page): + viewset = NotificationsViewSet() + fetch_user_notifications_data = { + "user": user_id, + "current_page" : current_page, + "items_per_page" : items_per_page + } + fetch_user_notifications_serializer = FetchUserNotificationsSerializer( + data = fetch_user_notifications_data + ) + + if fetch_user_notifications_serializer.is_valid(): + response = viewset.FetchUserNotifications( + request = type( + 'Request', + ( + object, + ), + { + 'data' : fetch_user_notifications_serializer.validated_data + } + ) + ) + + return response + + return None + + +# Mark user notification(s) as read +def MarkUserNotificationsAsRead(user_id, notification_ids = []): + viewset = NotificationsViewSet() + mark_notification_as_viewed_data = { + "user": user_id, + "notification_ids": notification_ids + } + mark_notification_as_viewed_serializer = MarkNotificationAsViewedSerializer( + data = mark_notification_as_viewed_data + ) + + if mark_notification_as_viewed_serializer.is_valid(): + response = viewset.MarkNotificationAsViewed( + request = type( + 'Request', + ( + object, + ), + { + 'data' : mark_notification_as_viewed_serializer.validated_data + } + ) + ) + + return response + + return None \ No newline at end of file diff --git a/cvat/apps/notifications/serializers.py b/cvat/apps/notifications/serializers.py index 6b824284a348..af3272acf727 100644 --- a/cvat/apps/notifications/serializers.py +++ b/cvat/apps/notifications/serializers.py @@ -45,4 +45,6 @@ class MarkNotificationAsViewedSerializer(serializers.Serializer): class FetchUserNotificationsSerializer(serializers.Serializer): - user = serializers.IntegerField() \ No newline at end of file + user = serializers.IntegerField() + current_page = serializers.IntegerField() + items_per_page = serializers.IntegerField() \ No newline at end of file diff --git a/cvat/apps/notifications/urls.py b/cvat/apps/notifications/urls.py new file mode 100644 index 000000000000..1620e9232384 --- /dev/null +++ b/cvat/apps/notifications/urls.py @@ -0,0 +1,21 @@ +from django.urls import path +from .views import NotificationsViewSet + + +notifications_viewset = NotificationsViewSet.as_view({ + 'post': 'SendNotification' +}) + +fetch_notifications_viewset = NotificationsViewSet.as_view({ + 'post': 'FetchUserNotifications' +}) + +mark_all_read_viewset = NotificationsViewSet.as_view({ + 'post': 'MarkNotificationAsViewed' +}) + +urlpatterns = [ + path('notifications/send', notifications_viewset, name='send-notification'), + path('notifications/fetch', fetch_notifications_viewset, name='fetch-user-notifications'), + path('notifications/markallread', mark_all_read_viewset, name='mark-all-read'), +] \ No newline at end of file diff --git a/cvat/apps/notifications/views.py b/cvat/apps/notifications/views.py index 1ec475b356cc..5c34b8404dbe 100644 --- a/cvat/apps/notifications/views.py +++ b/cvat/apps/notifications/views.py @@ -1,18 +1,39 @@ from django.shortcuts import render from django.utils import timezone +from django.core.paginator import EmptyPage from rest_framework import status, viewsets from rest_framework.request import Request from rest_framework.response import Response +from rest_framework.pagination import PageNumberPagination from .models import * from .serializers import * import json import traceback -# Create your views here. -## Usage + +## Pagination +class CustomPagination(PageNumberPagination): + def paginate_queryset(self, queryset, request, view = None): + page_size = request.data.get('items_per_page', 10) + page_number = request.data.get('current_page', 1) + self.page_size = page_size + paginator = self.django_paginator_class(queryset, page_size) + + try: + self.page = paginator.page(page_number) + except EmptyPage: + return None + + if int(page_number) > paginator.num_pages: + return None + + return list(self.page) + + +## Notification class NotificationsViewSet(viewsets.ViewSet): isAuthorized = True @@ -44,7 +65,7 @@ def AddNotification(self, data): "data": {}, "error": error }, - status=status.HTTP_500_INTERNAL_SERVER_ERROR + status = status.HTTP_500_INTERNAL_SERVER_ERROR ) else: return Response( @@ -54,7 +75,7 @@ def AddNotification(self, data): "data": serializer.errors, "error": None }, - status=status.HTTP_400_BAD_REQUEST + status = status.HTTP_400_BAD_REQUEST ) def SendNotification(self, request: Request): @@ -83,7 +104,7 @@ def SendNotification(self, request: Request): "data": serializer.errors, "error": None }, - status=status.HTTP_400_BAD_REQUEST + status = status.HTTP_400_BAD_REQUEST ) except Exception as e: error = traceback.format_exc() @@ -94,7 +115,7 @@ def SendNotification(self, request: Request): "data": {}, "error": error }, - status=status.HTTP_500_INTERNAL_SERVER_ERROR + status = status.HTTP_500_INTERNAL_SERVER_ERROR ) def SendUserNotifications(self, notification, user_id): @@ -113,7 +134,7 @@ def SendUserNotifications(self, notification, user_id): "data": {}, "error": None }, - status=status.HTTP_201_CREATED + status = status.HTTP_201_CREATED ) except User.DoesNotExist: return Response( @@ -123,7 +144,7 @@ def SendUserNotifications(self, notification, user_id): "data": {}, "error": None }, - status=status.HTTP_404_NOT_FOUND + status = status.HTTP_404_NOT_FOUND ) except Exception as e: error = traceback.format_exc() @@ -134,7 +155,7 @@ def SendUserNotifications(self, notification, user_id): "data": {}, "error": error }, - status=status.HTTP_500_INTERNAL_SERVER_ERROR + status = status.HTTP_500_INTERNAL_SERVER_ERROR ) def SendOrganizationNotifications(self, notification, data): @@ -157,7 +178,7 @@ def SendOrganizationNotifications(self, notification, data): "data": {}, "error": None }, - status=status.HTTP_200_OK + status = status.HTTP_200_OK ) else: return Response( @@ -167,7 +188,7 @@ def SendOrganizationNotifications(self, notification, data): "data": {}, "error": errors }, - status=status.HTTP_504_GATEWAY_TIMEOUT + status = status.HTTP_504_GATEWAY_TIMEOUT ) except Organization.DoesNotExist: return Response( @@ -177,7 +198,7 @@ def SendOrganizationNotifications(self, notification, data): "data": {}, "error": None }, - status=status.HTTP_404_NOT_FOUND + status = status.HTTP_404_NOT_FOUND ) except Exception as e: error = traceback.format_exc() @@ -188,28 +209,48 @@ def SendOrganizationNotifications(self, notification, data): "data": {}, "error": error }, - status=status.HTTP_500_INTERNAL_SERVER_ERROR + status = status.HTTP_500_INTERNAL_SERVER_ERROR ) + def FetchUserNotifications(self, request: Request): try: data = request.data - serializer = FetchUserNotificationsSerializer(data=data) + serializer = FetchUserNotificationsSerializer(data = data) + if serializer.is_valid(): user_id = serializer.validated_data["user"] - notifications_status = NotificationStatus.objects.filter(user_id=user_id) - data = [UserNotificationDetailSerializer(noti_status.notification).data for noti_status in notifications_status] + notifications_status = NotificationStatus.objects.filter(user_id=user_id).order_by('-notification__created_at') + unread_count = notifications_status.filter(is_read=False).count() + + # Set up pagination + paginator = CustomPagination() + paginated_notifications = paginator.paginate_queryset(notifications_status, request) + + if paginated_notifications is None: + return Response( + { + "success": False, + "message": "No notifications available on this page.", + "data": None, + "error": None + }, + status = status.HTTP_400_BAD_REQUEST + ) + + serialized_notifications = [UserNotificationDetailSerializer(noti_status.notification).data for noti_status in paginated_notifications] return Response( { "success": True, "message": "User notifications fetched successfully.", "data": { - "notifications": data + "unread" : unread_count, + "notifications": serialized_notifications }, "error": None }, - status=status.HTTP_200_OK + status = status.HTTP_200_OK ) else: return Response( @@ -219,10 +260,11 @@ def FetchUserNotifications(self, request: Request): "data": serializer.errors, "error": None }, - status=status.HTTP_400_BAD_REQUEST + status = status.HTTP_400_BAD_REQUEST ) except Exception as e: error = traceback.format_exc() + print(error) return Response( { "success": False, @@ -230,7 +272,7 @@ def FetchUserNotifications(self, request: Request): "data": {}, "error": error }, - status=status.HTTP_500_INTERNAL_SERVER_ERROR + status = status.HTTP_500_INTERNAL_SERVER_ERROR ) def MarkNotificationAsViewed(self, request: Request): @@ -252,7 +294,7 @@ def MarkNotificationAsViewed(self, request: Request): "data": {}, "error": None }, - status=status.HTTP_404_NOT_FOUND + status = status.HTTP_404_NOT_FOUND ) return Response( @@ -262,7 +304,7 @@ def MarkNotificationAsViewed(self, request: Request): "data": {}, "error": None }, - status=status.HTTP_200_OK + status = status.HTTP_200_OK ) else: return Response( @@ -272,7 +314,7 @@ def MarkNotificationAsViewed(self, request: Request): "data": serializer.errors, "error": None }, - status=status.HTTP_400_BAD_REQUEST + status = status.HTTP_400_BAD_REQUEST ) except Exception as e: error = traceback.format_exc() @@ -283,5 +325,5 @@ def MarkNotificationAsViewed(self, request: Request): "data": {}, "error": error }, - status=status.HTTP_500_INTERNAL_SERVER_ERROR + status = status.HTTP_500_INTERNAL_SERVER_ERROR ) \ No newline at end of file diff --git a/cvat/apps/organizations/views.py b/cvat/apps/organizations/views.py index d65327623f46..2b6e4e67e7d8 100644 --- a/cvat/apps/organizations/views.py +++ b/cvat/apps/organizations/views.py @@ -253,60 +253,7 @@ def perform_create(self, serializer): request=self.request, ) - ## Send Notification - from ..notifications.views import NotificationsViewSet - from ..notifications.serializers import (SendNotificationSerializer, FetchUserNotificationsSerializer, MarkNotificationAsViewedSerializer) - - viewset = NotificationsViewSet() - - # Test Sending Notification to Organization - send_notification_data = { - 'org': self.request.iam_context['organization'].id, - 'title': 'Test Notification', - 'message': 'This is a test message', - 'notification_type': 'info', - } - send_notification_serializer = SendNotificationSerializer(data=send_notification_data) - if send_notification_serializer.is_valid(): - response = viewset.SendNotification( - request=type('Request', (object,), {'data': send_notification_serializer.validated_data}) - ) - - # Test Sending Notification to User - send_notification_data_user = { - 'user': self.request.user.id, - 'title': 'Test Notification User only', - 'message': 'This is a test message', - 'notification_type': 'info', - } - send_notification_serializer_user = SendNotificationSerializer(data=send_notification_data_user) - if send_notification_serializer_user.is_valid(): - response = viewset.SendNotification( - request=type('Request', (object,), {'data': send_notification_serializer_user.validated_data}) - ) - - # Test Fetching Notifications - fetch_user_notifications_data = { - "user": self.request.user.id - } - fetch_user_notifications_serializer = FetchUserNotificationsSerializer(data=fetch_user_notifications_data) - if fetch_user_notifications_serializer.is_valid(): - response = viewset.FetchUserNotifications( - request=type('Request', (object,), {'data': fetch_user_notifications_serializer.validated_data}) - ) - notifications = response.data["data"]["notifications"] - notification_ids = [notification["id"] for notification in notifications] - # Test Marking Notifications as Viewed - mark_notification_as_viewed_data = { - "user": self.request.user.id, - "notification_ids": notification_ids - } - mark_notification_as_viewed_serializer = MarkNotificationAsViewedSerializer(data=mark_notification_as_viewed_data) - if mark_notification_as_viewed_serializer.is_valid(): - response = viewset.MarkNotificationAsViewed( - request=type('Request', (object,), {'data': mark_notification_as_viewed_serializer.validated_data}) - ) def perform_update(self, serializer): @@ -330,6 +277,16 @@ def accept(self, request, pk): response_serializer = AcceptInvitationReadSerializer(data={'organization_slug': invitation.membership.organization.slug}) response_serializer.is_valid(raise_exception=True) + ## Notifications + from ..notifications.api import SendNotificationToOrganisationUsers + + notification_response = SendNotificationToOrganisationUsers( + self.request.iam_context['organization'].id, + f"{self.request.user.username} joined to {self.request.iam_context['organization'].name}", + "Hey guys an idiot joined the organization.", + "info" + ) + return Response(status=status.HTTP_200_OK, data=response_serializer.data) except Invitation.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND, data="This invitation does not exist. Please contact organization owner.") diff --git a/cvat/urls.py b/cvat/urls.py index 2377e748ed5f..396f3a1b4f6b 100644 --- a/cvat/urls.py +++ b/cvat/urls.py @@ -52,5 +52,5 @@ if apps.is_installed('cvat.apps.analytics_report'): urlpatterns.append(path('api/', include('cvat.apps.analytics_report.urls'))) -if apps.is_installed('cvat.apps.Notifications'): - urlpatterns.append(path('api/', include('cvat.apps.Notifications.urls'))) \ No newline at end of file +if apps.is_installed('cvat.apps.notifications'): + urlpatterns.append(path('api/', include('cvat.apps.notifications.urls'))) \ No newline at end of file