From 6e1272692ca34f3b0f5cc99025d20aba19345b9f Mon Sep 17 00:00:00 2001 From: Vignesh Goswami Date: Sun, 1 Sep 2024 12:34:05 +0530 Subject: [PATCH 01/17] Initial Commit --- cvat/apps/Notifications/__init__.py | 0 cvat/apps/Notifications/admin.py | 3 + cvat/apps/Notifications/apps.py | 6 + .../apps/Notifications/migrations/__init__.py | 0 cvat/apps/Notifications/models.py | 26 ++ cvat/apps/Notifications/tests.py | 3 + cvat/apps/Notifications/urls.py | 9 + cvat/apps/Notifications/views.py | 233 ++++++++++++++++++ cvat/settings/base.py | 1 + cvat/urls.py | 3 + datumaro | 1 + rsa | 49 ++++ rsa.pub | 1 + 13 files changed, 335 insertions(+) create mode 100644 cvat/apps/Notifications/__init__.py create mode 100644 cvat/apps/Notifications/admin.py create mode 100644 cvat/apps/Notifications/apps.py create mode 100644 cvat/apps/Notifications/migrations/__init__.py create mode 100644 cvat/apps/Notifications/models.py create mode 100644 cvat/apps/Notifications/tests.py create mode 100644 cvat/apps/Notifications/urls.py create mode 100644 cvat/apps/Notifications/views.py create mode 160000 datumaro create mode 100644 rsa create mode 100644 rsa.pub diff --git a/cvat/apps/Notifications/__init__.py b/cvat/apps/Notifications/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/cvat/apps/Notifications/admin.py b/cvat/apps/Notifications/admin.py new file mode 100644 index 000000000000..8c38f3f3dad5 --- /dev/null +++ b/cvat/apps/Notifications/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/cvat/apps/Notifications/apps.py b/cvat/apps/Notifications/apps.py new file mode 100644 index 000000000000..07e75167bbe2 --- /dev/null +++ b/cvat/apps/Notifications/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class NotificationsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "Notifications" diff --git a/cvat/apps/Notifications/migrations/__init__.py b/cvat/apps/Notifications/migrations/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/cvat/apps/Notifications/models.py b/cvat/apps/Notifications/models.py new file mode 100644 index 000000000000..6d2428951159 --- /dev/null +++ b/cvat/apps/Notifications/models.py @@ -0,0 +1,26 @@ +from django.db.models import * + +from django.contrib.auth.models import User +# Create your models here. + +class Notifications(Model): + title = CharField(max_length=255) + message = TextField() + extra_data = JSONField(blank=True, null=True) + files = FileField(upload_to='notifications/files/', blank=True, null=True) + url = URLField(blank=True, null=True) + created_at = DateTimeField(auto_now_add=True) + is_read = BooleanField(default=False) + read_at = DateTimeField(blank=True, null=True) + + recipient = ManyToManyField(User, related_name='notifications') + + notification_type = CharField(max_length=50, choices=[ + ('info', 'Info'), + ('warning', 'Warning'), + ('success', 'Success'), + ('error', 'Error') + ]) + + def __str__(self): + return f"Notification - {self.title}" \ No newline at end of file diff --git a/cvat/apps/Notifications/tests.py b/cvat/apps/Notifications/tests.py new file mode 100644 index 000000000000..7ce503c2dd97 --- /dev/null +++ b/cvat/apps/Notifications/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/cvat/apps/Notifications/urls.py b/cvat/apps/Notifications/urls.py new file mode 100644 index 000000000000..202c72375882 --- /dev/null +++ b/cvat/apps/Notifications/urls.py @@ -0,0 +1,9 @@ +from django.urls import path, include +from rest_framework import routers + +from .views import * + +router = routers.DefaultRouter(trailing_slash=False) +router.register('notifications', NotificationsViewSet, basename='notifications') + +urlpatterns = router.urls diff --git a/cvat/apps/Notifications/views.py b/cvat/apps/Notifications/views.py new file mode 100644 index 000000000000..92d95bdc90d4 --- /dev/null +++ b/cvat/apps/Notifications/views.py @@ -0,0 +1,233 @@ +from django.shortcuts import render +from django.utils import timezone + +from rest_framework import status, viewsets +from rest_framework.response import Response + +import traceback + +from .models import * +# Create your views here. + + +class NotificationsViewSet(viewsets.ViewSet): + isAuthorized = True + + def SendNotification(self, request): + try: + req = request.data + + if "user" in req: + user = req["user"] + response = self.SendUserNotifications(user, req) + elif "org" in req: + response = self.SendOrganizationNotifications(req) + else: + return Response( + { + "success" : False, + "message" : "Invalid request data. 'user' or 'org' key is required.", + "data" : {}, + "error" : None + }, + status = status.HTTP_400_BAD_REQUEST + ) + + return response + except Exception as e: + error = traceback.format_exc() + + return Response( + { + "success" : False, + "message" : "An error occurred while sending notification.", + "data" : {}, + "error" : error + }, + status = status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + + def SendUserNotifications(self, usr, req): + try: + user = User.objects.get(id=usr) + notification = Notifications.objects.create( + title = req.get('title'), + message = req.get('message'), + notification_type = req.get('notification_type'), + url = req.get('url', ''), + extra_data = req.get('extra_data', {}), + files = req.get('files', None), + ) + notification.recipient.add(user) + notification.save() + + return Response( + { + "success" : True, + "message" : "Notification sent successfully.", + "data" : {}, + "error" : None + }, + status = status.HTTP_201_CREATED + ) + except User.DoesNotExist: + return Response( + { + "success" : False, + "message" : f"User with id {usr} does not exist.", + "data" : {}, + "error" : None + }, + status = status.HTTP_404_NOT_FOUND + ) + except Exception as e: + error = traceback.format_exc() + + return Response( + { + "success" : False, + "message" : "An error occurred while sending user notification.", + "data" : {}, + "error" : error + }, + status = status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + + def SendOrganizationNotifications(self, req): + try: + organization = Organization.objects.get(id=req["org"]) # check for organization + users = organization.user_set.all() + errors = [] + + for user in users: + response = self.SendUserNotifications(user.id, req) + if not response.data.get("success"): + errors.append(f"Error occurred while sending notification to user ({user.username}). Error: {response.data.get('error')}") + + if not errors: + return Response( + { + "success" : True, + "message" : "Notifications sent successfully.", + "data" : {}, + "error" : None + }, + status = status.HTTP_200_OK + ) + else: + return Response( + { + "success" : False, + "message" : "Unable to send notifications to one or more users.", + "data" : {}, + "error" : errors + }, + status = status.HTTP_504_GATEWAY_TIMEOUT + ) + except Organization.DoesNotExist: + return Response( + { + "success" : False, + "message" : f"Organization with id {req['org']} does not exist.", + "data" : {}, + "error" : None + }, + status = status.HTTP_404_NOT_FOUND + ) + except Exception as e: + error = traceback.format_exc() + + return Response( + { + "success" : False, + "message" : "An error occurred while sending organization notifications.", + "data" : {}, + "error" : error + }, + status = status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + + def FetchUserNotifications(self, request): + try: + user = request.user + notifications = Notifications.objects.filter(recipient=user) + data = [] + + for notification in notifications: + noti = { + "title" : notification.title, + "message" : notification.message, + "url" : notification.url, + "created_at" : notification.created_at, + "is_read" : notification.is_read, + "notification_type" : notification.notification_type, + "files" : notification.files.url if notification.files else None, + } + data.append(noti) + + return Response( + { + "success" : True, + "message" : "User notifications fetched successfully.", + "data" : { + "notifications" : data + }, + "error" : None + }, + status = status.HTTP_200_OK + ) + except Exception as e: + error = traceback.format_exc() + + return Response( + { + "success" : False, + "message" : "An error occurred while fetching notifications.", + "data" : {}, + "error" : error + }, + status = status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + + def MarkNotificationAsViewed(self, request): + try: + notification_id = request.data.get('notification_id') + notification = Notifications.objects.get(id=notification_id, recipient=request.user) + notification.is_read = True + notification.read_at = timezone.now() + notification.save() + + return Response( + { + "success" : True, + "message" : "Notification marked as viewed.", + "data" : {}, + "error" : None + }, + status = status.HTTP_200_OK + ) + except Notifications.DoesNotExist: + return Response( + { + "success" : False, + "message" : f"Notification with id {notification_id} does not exist or does not belong to you.", + "data" : {}, + "error" : None + }, + status = status.HTTP_404_NOT_FOUND + ) + except Exception as e: + error = traceback.format_exc() + return Response( + { + "success" : False, + "message" : "An error occurred while marking notification as viewed.", + "data" : {}, + "error" : error + }, + status = status.HTTP_500_INTERNAL_SERVER_ERROR + ) \ No newline at end of file diff --git a/cvat/settings/base.py b/cvat/settings/base.py index 3e4d610915bd..f534d6255d1e 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -117,6 +117,7 @@ def generate_secret_key(): 'cvat.apps.events', 'cvat.apps.quality_control', 'cvat.apps.analytics_report', + 'cvat.apps.Notifications' ] SITE_ID = 1 diff --git a/cvat/urls.py b/cvat/urls.py index 144ed619f766..7bcf3e03ca2c 100644 --- a/cvat/urls.py +++ b/cvat/urls.py @@ -51,3 +51,6 @@ 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('', include('cvat.apps.Notifications.urls'))) \ No newline at end of file diff --git a/datumaro b/datumaro new file mode 160000 index 000000000000..125840fc6b28 --- /dev/null +++ b/datumaro @@ -0,0 +1 @@ +Subproject commit 125840fc6b28875cce4c85626a5c36bb9e0d2a83 diff --git a/rsa b/rsa new file mode 100644 index 000000000000..6646c14e49a7 --- /dev/null +++ b/rsa @@ -0,0 +1,49 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +-----END OPENSSH PRIVATE KEY----- diff --git a/rsa.pub b/rsa.pub new file mode 100644 index 000000000000..cf968f886aee --- /dev/null +++ b/rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC7shCjqEeoikvLRIf0JH4W/S6mDSyujE4dw7DNeuU1KR/Cql2i/garB7VKvE8F0lEDS0o03hrPHfXu1q4FsbcBey0NvVYgpmge2+0ecLxMsKM+TkmJaAeapUyK4cWHSUkcu0jpGvQ9HybqwEh/o2nALTz+QLnQm0ymQXxAtvRb3zjLggSTt6IFf7biHLKOIwR4pCVj/0PkLlja2DDSEtMenC+sj6x08+eHdnxmCrS5QRSarQOGZEO34ZRBM0nQ15bf5/WDcDAEkJYEGDhaXsGBEIiRfZF0rsjIU6Fp1DwpwIS3Nnc8Fh0stxioABEDLynvhd09g78NE/RYQnHPw+dBU4puFdSakoWunM6MMpudivOcNdiJNq8uOUotgGHJePIF5jc/9h6Gevu3b/Ia9bhhLvR/NSsCuy3v7NwY4rhZQRuVqEX2cBxwVbSHWUM91Os57fFieRAQuTLmIbjEQSCv3UfO34Ia8pmfyAfr3PgLkQV0jBZbLz/CbdZT9VzjQ58jJ1Sd+rGI4GNwjtjh+vQbGz5vLMsQoUcORLD7QXG4OPzb4yoLSjC4iZghszcmkr+DP3t2JH8ZbwCaZMSWNiEaGX3Cymsaw6VZQot0Tz+cqUnaoB/hd3GLn+OmoE/OsVfwQvCQeECkU+IDMSIlHlzOjgAT9noj4NcAB4MoFdBNGw== vignesh20152@iiitd.ac.in From f1597a8adb92a747f705f9d03a949009d0e7f62b Mon Sep 17 00:00:00 2001 From: Vignesh Goswami Date: Sun, 1 Sep 2024 12:56:52 +0530 Subject: [PATCH 02/17] 1.1 --- .gitignore | 4 ++++ cvat/apps/Notifications/models.py | 2 -- cvat/apps/Notifications/views.py | 2 -- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 9736baa80a3f..8300e608bd71 100644 --- a/.gitignore +++ b/.gitignore @@ -66,3 +66,7 @@ cvat-core/reports # produced by cvat/apps/iam/rules/tests/generate_tests.py /cvat/apps/*/rules/*_test.gen.rego + +# Custom +rsa +rsa.pub \ No newline at end of file diff --git a/cvat/apps/Notifications/models.py b/cvat/apps/Notifications/models.py index 6d2428951159..1a83649e6b50 100644 --- a/cvat/apps/Notifications/models.py +++ b/cvat/apps/Notifications/models.py @@ -7,8 +7,6 @@ class Notifications(Model): title = CharField(max_length=255) message = TextField() extra_data = JSONField(blank=True, null=True) - files = FileField(upload_to='notifications/files/', blank=True, null=True) - url = URLField(blank=True, null=True) created_at = DateTimeField(auto_now_add=True) is_read = BooleanField(default=False) read_at = DateTimeField(blank=True, null=True) diff --git a/cvat/apps/Notifications/views.py b/cvat/apps/Notifications/views.py index 92d95bdc90d4..b4f27cded1a6 100644 --- a/cvat/apps/Notifications/views.py +++ b/cvat/apps/Notifications/views.py @@ -55,9 +55,7 @@ def SendUserNotifications(self, usr, req): title = req.get('title'), message = req.get('message'), notification_type = req.get('notification_type'), - url = req.get('url', ''), extra_data = req.get('extra_data', {}), - files = req.get('files', None), ) notification.recipient.add(user) notification.save() From 255c3d0737000f6883d8dc915d89e00c96a4705a Mon Sep 17 00:00:00 2001 From: Vignesh Goswami Date: Sun, 1 Sep 2024 13:04:21 +0530 Subject: [PATCH 03/17] 1.2 --- cvat/apps/Notifications/views.py | 15 ++++++++++ cvat/urls.py | 2 +- rsa | 49 -------------------------------- rsa.pub | 1 - 4 files changed, 16 insertions(+), 51 deletions(-) delete mode 100644 rsa delete mode 100644 rsa.pub diff --git a/cvat/apps/Notifications/views.py b/cvat/apps/Notifications/views.py index b4f27cded1a6..aeff49b0ce58 100644 --- a/cvat/apps/Notifications/views.py +++ b/cvat/apps/Notifications/views.py @@ -13,6 +13,21 @@ class NotificationsViewSet(viewsets.ViewSet): isAuthorized = True + + # Usage + # from rest_framework.test import APIRequestFactory + + # request_data = { + # "user": 1, + # "title": "Test Notification", + # "message": "This is a test notification message.", + # "notification_type": "info", + # "extra_data": {"key": "value"} + # } + # factory = APIRequestFactory() + # req = factory.post('/api/notifications', request_data, format='json') + # notifications_view = NotificationsViewSet.as_view({'post': 'SendNotification'}) + # response = notifications_view(req) def SendNotification(self, request): try: req = request.data diff --git a/cvat/urls.py b/cvat/urls.py index 7bcf3e03ca2c..2377e748ed5f 100644 --- a/cvat/urls.py +++ b/cvat/urls.py @@ -53,4 +53,4 @@ urlpatterns.append(path('api/', include('cvat.apps.analytics_report.urls'))) if apps.is_installed('cvat.apps.Notifications'): - urlpatterns.append(path('', include('cvat.apps.Notifications.urls'))) \ No newline at end of file + urlpatterns.append(path('api/', include('cvat.apps.Notifications.urls'))) \ No newline at end of file diff --git a/rsa b/rsa deleted file mode 100644 index 6646c14e49a7..000000000000 --- a/rsa +++ /dev/null @@ -1,49 +0,0 @@ ------BEGIN OPENSSH PRIVATE KEY----- -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 ------END OPENSSH PRIVATE KEY----- diff --git a/rsa.pub b/rsa.pub deleted file mode 100644 index cf968f886aee..000000000000 --- a/rsa.pub +++ /dev/null @@ -1 +0,0 @@ -ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC7shCjqEeoikvLRIf0JH4W/S6mDSyujE4dw7DNeuU1KR/Cql2i/garB7VKvE8F0lEDS0o03hrPHfXu1q4FsbcBey0NvVYgpmge2+0ecLxMsKM+TkmJaAeapUyK4cWHSUkcu0jpGvQ9HybqwEh/o2nALTz+QLnQm0ymQXxAtvRb3zjLggSTt6IFf7biHLKOIwR4pCVj/0PkLlja2DDSEtMenC+sj6x08+eHdnxmCrS5QRSarQOGZEO34ZRBM0nQ15bf5/WDcDAEkJYEGDhaXsGBEIiRfZF0rsjIU6Fp1DwpwIS3Nnc8Fh0stxioABEDLynvhd09g78NE/RYQnHPw+dBU4puFdSakoWunM6MMpudivOcNdiJNq8uOUotgGHJePIF5jc/9h6Gevu3b/Ia9bhhLvR/NSsCuy3v7NwY4rhZQRuVqEX2cBxwVbSHWUM91Os57fFieRAQuTLmIbjEQSCv3UfO34Ia8pmfyAfr3PgLkQV0jBZbLz/CbdZT9VzjQ58jJ1Sd+rGI4GNwjtjh+vQbGz5vLMsQoUcORLD7QXG4OPzb4yoLSjC4iZghszcmkr+DP3t2JH8ZbwCaZMSWNiEaGX3Cymsaw6VZQot0Tz+cqUnaoB/hd3GLn+OmoE/OsVfwQvCQeECkU+IDMSIlHlzOjgAT9noj4NcAB4MoFdBNGw== vignesh20152@iiitd.ac.in From 669e733e1896c8f523c73636b2d23f17d5818ffc Mon Sep 17 00:00:00 2001 From: Vignesh Goswami Date: Sun, 1 Sep 2024 13:21:57 +0530 Subject: [PATCH 04/17] 1.3 --- cvat/apps/Notifications/models.py | 1 + cvat/apps/Notifications/views.py | 8 +++++--- cvat/apps/engine/views.py | 18 ++++++++++++++++++ cvat/apps/organizations/views.py | 19 +++++++++++++++++++ 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/cvat/apps/Notifications/models.py b/cvat/apps/Notifications/models.py index 1a83649e6b50..6c09f675c260 100644 --- a/cvat/apps/Notifications/models.py +++ b/cvat/apps/Notifications/models.py @@ -1,6 +1,7 @@ from django.db.models import * from django.contrib.auth.models import User +from ..organizations.models import * # Create your models here. class Notifications(Model): diff --git a/cvat/apps/Notifications/views.py b/cvat/apps/Notifications/views.py index aeff49b0ce58..60693ec75874 100644 --- a/cvat/apps/Notifications/views.py +++ b/cvat/apps/Notifications/views.py @@ -110,12 +110,14 @@ def SendUserNotifications(self, usr, req): def SendOrganizationNotifications(self, req): try: - organization = Organization.objects.get(id=req["org"]) # check for organization - users = organization.user_set.all() + organization = Organization.objects.get(id=req["org"]) + members = organization.members.filter(is_active=True) errors = [] - for user in users: + for member in members: + user = member.user response = self.SendUserNotifications(user.id, req) + if not response.data.get("success"): errors.append(f"Error occurred while sending notification to user ({user.username}). Error: {response.data.get('error')}") diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index 7a42f2986332..34a6f05976b8 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -2155,6 +2155,24 @@ 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") diff --git a/cvat/apps/organizations/views.py b/cvat/apps/organizations/views.py index 11b92b29cad8..f29b902ae914 100644 --- a/cvat/apps/organizations/views.py +++ b/cvat/apps/organizations/views.py @@ -272,6 +272,25 @@ def accept(self, request, pk): invitation.accept() response_serializer = AcceptInvitationReadSerializer(data={'organization_slug': invitation.membership.organization.slug}) response_serializer.is_valid(raise_exception=True) + + ## Send Notification + from rest_framework.test import APIRequestFactory + from ..Notifications.views import NotificationsViewSet + + request_data = { + "org": invitation.membership.organization.id, + "title": "New member Joined", + "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) + 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.") From 35d512f639c33b22c9c9a51c132afd27af2b5887 Mon Sep 17 00:00:00 2001 From: Vignesh Goswami Date: Sun, 1 Sep 2024 13:54:41 +0530 Subject: [PATCH 05/17] 1.4 --- cvat/apps/Notifications/apps.py | 6 --- cvat/apps/Notifications/urls.py | 9 ---- cvat/apps/engine/views.py | 2 +- .../__init__.py | 0 .../{Notifications => notifications}/admin.py | 0 cvat/apps/notifications/apps.py | 9 ++++ .../notifications/migrations/0001_initial.py | 54 +++++++++++++++++++ .../migrations/__init__.py | 0 .../models.py | 0 cvat/apps/notifications/permissions.py | 38 +++++++++++++ .../{Notifications => notifications}/tests.py | 0 cvat/apps/notifications/urls.py | 10 ++++ .../{Notifications => notifications}/views.py | 0 cvat/apps/organizations/views.py | 2 +- cvat/settings/base.py | 6 +-- 15 files changed, 116 insertions(+), 20 deletions(-) delete mode 100644 cvat/apps/Notifications/apps.py delete mode 100644 cvat/apps/Notifications/urls.py rename cvat/apps/{Notifications => notifications}/__init__.py (100%) rename cvat/apps/{Notifications => notifications}/admin.py (100%) create mode 100644 cvat/apps/notifications/apps.py create mode 100644 cvat/apps/notifications/migrations/0001_initial.py rename cvat/apps/{Notifications => notifications}/migrations/__init__.py (100%) rename cvat/apps/{Notifications => notifications}/models.py (100%) create mode 100644 cvat/apps/notifications/permissions.py rename cvat/apps/{Notifications => notifications}/tests.py (100%) create mode 100644 cvat/apps/notifications/urls.py rename cvat/apps/{Notifications => notifications}/views.py (100%) diff --git a/cvat/apps/Notifications/apps.py b/cvat/apps/Notifications/apps.py deleted file mode 100644 index 07e75167bbe2..000000000000 --- a/cvat/apps/Notifications/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class NotificationsConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "Notifications" diff --git a/cvat/apps/Notifications/urls.py b/cvat/apps/Notifications/urls.py deleted file mode 100644 index 202c72375882..000000000000 --- a/cvat/apps/Notifications/urls.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.urls import path, include -from rest_framework import routers - -from .views import * - -router = routers.DefaultRouter(trailing_slash=False) -router.register('notifications', NotificationsViewSet, basename='notifications') - -urlpatterns = router.urls diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index 34a6f05976b8..37964dc2d592 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -2157,7 +2157,7 @@ class AIAudioAnnotationViewSet(viewsets.ModelViewSet): def send_annotation_email(self, request, template_name, err=None): ## Send Notifications from rest_framework.test import APIRequestFactory - from ..Notifications.views import NotificationsViewSet + from ..notifications.views import NotificationsViewSet job_id = request.data.get('jobId') request_data = { "user": self.request.user.id, diff --git a/cvat/apps/Notifications/__init__.py b/cvat/apps/notifications/__init__.py similarity index 100% rename from cvat/apps/Notifications/__init__.py rename to cvat/apps/notifications/__init__.py diff --git a/cvat/apps/Notifications/admin.py b/cvat/apps/notifications/admin.py similarity index 100% rename from cvat/apps/Notifications/admin.py rename to cvat/apps/notifications/admin.py diff --git a/cvat/apps/notifications/apps.py b/cvat/apps/notifications/apps.py new file mode 100644 index 000000000000..6d66d17830b5 --- /dev/null +++ b/cvat/apps/notifications/apps.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class NotificationsConfig(AppConfig): + name = 'cvat.apps.notifications' + + def ready(self) -> None: + from cvat.apps.iam.permissions import load_app_permissions + load_app_permissions(self) diff --git a/cvat/apps/notifications/migrations/0001_initial.py b/cvat/apps/notifications/migrations/0001_initial.py new file mode 100644 index 000000000000..727b8f27ccea --- /dev/null +++ b/cvat/apps/notifications/migrations/0001_initial.py @@ -0,0 +1,54 @@ +# Generated by Django 4.2.14 on 2024-09-01 08:23 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Notifications", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=255)), + ("message", models.TextField()), + ("extra_data", models.JSONField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("is_read", models.BooleanField(default=False)), + ("read_at", models.DateTimeField(blank=True, null=True)), + ( + "notification_type", + models.CharField( + choices=[ + ("info", "Info"), + ("warning", "Warning"), + ("success", "Success"), + ("error", "Error"), + ], + max_length=50, + ), + ), + ( + "recipient", + models.ManyToManyField( + related_name="notifications", to=settings.AUTH_USER_MODEL + ), + ), + ], + ), + ] diff --git a/cvat/apps/Notifications/migrations/__init__.py b/cvat/apps/notifications/migrations/__init__.py similarity index 100% rename from cvat/apps/Notifications/migrations/__init__.py rename to cvat/apps/notifications/migrations/__init__.py diff --git a/cvat/apps/Notifications/models.py b/cvat/apps/notifications/models.py similarity index 100% rename from cvat/apps/Notifications/models.py rename to cvat/apps/notifications/models.py diff --git a/cvat/apps/notifications/permissions.py b/cvat/apps/notifications/permissions.py new file mode 100644 index 000000000000..c4000217c128 --- /dev/null +++ b/cvat/apps/notifications/permissions.py @@ -0,0 +1,38 @@ +# notifications/permissions.py + +from django.conf import settings +from cvat.apps.iam.permissions import OpenPolicyAgentPermission, StrEnum + +class NotificationPermission(OpenPolicyAgentPermission): + class Scopes(StrEnum): + VIEW = 'view' + SEND = 'send' + MARK_AS_READ = 'mark_as_read' + + @classmethod + def create(cls, request, view, obj, iam_context): + permissions = [] + for scope in cls.get_scopes(request, view, obj): + perm = cls.create_base_perm(request, view, scope, iam_context, obj) + permissions.append(perm) + return permissions + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.url = settings.IAM_OPA_DATA_URL + '/notifications/allow' + + @staticmethod + def get_scopes(request, view, obj): + Scopes = __class__.Scopes + if view.action == 'SendNotification': + return [Scopes.SEND] + elif view.action == 'FetchUserNotifications': + return [Scopes.VIEW] + elif view.action == 'MarkNotificationAsViewed': + return [Scopes.MARK_AS_READ] + return [] + + def get_resource(self): + return { + 'visibility': 'public' if settings.RESTRICTIONS.get('notifications_visibility', True) else 'private', + } diff --git a/cvat/apps/Notifications/tests.py b/cvat/apps/notifications/tests.py similarity index 100% rename from cvat/apps/Notifications/tests.py rename to cvat/apps/notifications/tests.py diff --git a/cvat/apps/notifications/urls.py b/cvat/apps/notifications/urls.py new file mode 100644 index 000000000000..8ad120bed2cb --- /dev/null +++ b/cvat/apps/notifications/urls.py @@ -0,0 +1,10 @@ +from django.urls import path, include + +from .views import * + + +urlpatterns = [ + path('api/notifications/', NotificationsViewSet.as_view({'post': 'SendNotification'}), name='send-notification'), + path('api/notifications/fetch/', NotificationsViewSet.as_view({'get': 'FetchUserNotifications'}), name='fetch-notifications'), + path('api/notifications/mark-viewed/', NotificationsViewSet.as_view({'post': 'MarkNotificationAsViewed'}), name='mark-notification-viewed'), +] \ No newline at end of file diff --git a/cvat/apps/Notifications/views.py b/cvat/apps/notifications/views.py similarity index 100% rename from cvat/apps/Notifications/views.py rename to cvat/apps/notifications/views.py diff --git a/cvat/apps/organizations/views.py b/cvat/apps/organizations/views.py index f29b902ae914..3cd810abce54 100644 --- a/cvat/apps/organizations/views.py +++ b/cvat/apps/organizations/views.py @@ -275,7 +275,7 @@ def accept(self, request, pk): ## Send Notification from rest_framework.test import APIRequestFactory - from ..Notifications.views import NotificationsViewSet + from ..notifications.views import NotificationsViewSet request_data = { "org": invitation.membership.organization.id, diff --git a/cvat/settings/base.py b/cvat/settings/base.py index f534d6255d1e..12757f3f8b81 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -38,8 +38,8 @@ # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = str(Path(__file__).parents[2]) -ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',') -INTERNAL_IPS = ['127.0.0.1'] +ALLOWED_HOSTS = ["*"] +INTERNAL_IPS = ['127.0.0.1',] def generate_secret_key(): """ @@ -117,7 +117,7 @@ def generate_secret_key(): 'cvat.apps.events', 'cvat.apps.quality_control', 'cvat.apps.analytics_report', - 'cvat.apps.Notifications' + 'cvat.apps.notifications', ] SITE_ID = 1 From 38401fe14a0d672148b7fb6957f918e98f3e2d3f Mon Sep 17 00:00:00 2001 From: Vignesh Goswami <88871046+Vignesh16879@users.noreply.github.com> Date: Wed, 4 Sep 2024 14:17:13 +0530 Subject: [PATCH 06/17] Update views.py --- cvat/apps/notifications/views.py | 105 +++++++++++++++++++++---------- 1 file changed, 71 insertions(+), 34 deletions(-) diff --git a/cvat/apps/notifications/views.py b/cvat/apps/notifications/views.py index 60693ec75874..2c79bd38905f 100644 --- a/cvat/apps/notifications/views.py +++ b/cvat/apps/notifications/views.py @@ -9,34 +9,75 @@ from .models import * # Create your views here. +## Usage +# from rest_framework.test import APIRequestFactory +# request_data = { +# "user": 1, +# "title": "Test Notification", +# "message": "This is a test notification message.", +# "notification_type": "info", +# "extra_data": {"key": "value"} +# } +# factory = APIRequestFactory() +# req = factory.post('/api/notifications', request_data, format='json') +# notifications_view = NotificationsViewSet.as_view({'post': 'SendNotification'}) +# response = notifications_view(req) class NotificationsViewSet(viewsets.ViewSet): isAuthorized = True - # Usage - # from rest_framework.test import APIRequestFactory - - # request_data = { - # "user": 1, - # "title": "Test Notification", - # "message": "This is a test notification message.", - # "notification_type": "info", - # "extra_data": {"key": "value"} - # } - # factory = APIRequestFactory() - # req = factory.post('/api/notifications', request_data, format='json') - # notifications_view = NotificationsViewSet.as_view({'post': 'SendNotification'}) - # response = notifications_view(req) + def AddNotification(self, req): + try: + notification = Notifications.objects.create( + title = req.get('title'), + message = req.get('message'), + notification_type = req.get('notification_type'), + extra_data = req.get('extra_data', {}), + ) + notification.save() + + return Response( + { + "success" : True, + "message" : "An error occurred while saving notification.", + "data" : { + "notification" : notification + }, + "error" : error + } + ) + except Exception as e: + error = traceback.format_exc() + + return Response( + { + "success" : False, + "message" : "An error occurred while saving notification.", + "data" : {}, + "error" : error + }, + status = status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + def SendNotification(self, request): try: req = request.data - if "user" in req: - user = req["user"] - response = self.SendUserNotifications(user, req) - elif "org" in req: - response = self.SendOrganizationNotifications(req) + if "user" in req or "org" in req: + response = self.AAddNotification(req) + + if not response["success"]: + return response + + notification = response["data"]["notification"] + + if "user" in req: + user = req["user"] + response = self.SendUserNotifications(notification, user, req) + elif "org" in req: + response = self.SendOrganizationNotifications(notification, req) else: return Response( { @@ -48,10 +89,13 @@ def SendNotification(self, request): status = status.HTTP_400_BAD_REQUEST ) + if response["success"] == False: + self.try_delete_notification(notification) + return response except Exception as e: error = traceback.format_exc() - + return Response( { "success" : False, @@ -63,17 +107,10 @@ def SendNotification(self, request): ) - def SendUserNotifications(self, usr, req): + def SendUserNotifications(self, notification, usr, req): try: user = User.objects.get(id=usr) - notification = Notifications.objects.create( - title = req.get('title'), - message = req.get('message'), - notification_type = req.get('notification_type'), - extra_data = req.get('extra_data', {}), - ) notification.recipient.add(user) - notification.save() return Response( { @@ -96,7 +133,7 @@ def SendUserNotifications(self, usr, req): ) except Exception as e: error = traceback.format_exc() - + return Response( { "success" : False, @@ -108,7 +145,7 @@ def SendUserNotifications(self, usr, req): ) - def SendOrganizationNotifications(self, req): + def SendOrganizationNotifications(self, notification, req): try: organization = Organization.objects.get(id=req["org"]) members = organization.members.filter(is_active=True) @@ -117,7 +154,7 @@ def SendOrganizationNotifications(self, req): for member in members: user = member.user response = self.SendUserNotifications(user.id, req) - + if not response.data.get("success"): errors.append(f"Error occurred while sending notification to user ({user.username}). Error: {response.data.get('error')}") @@ -153,7 +190,7 @@ def SendOrganizationNotifications(self, req): ) except Exception as e: error = traceback.format_exc() - + return Response( { "success" : False, @@ -196,7 +233,7 @@ def FetchUserNotifications(self, request): ) except Exception as e: error = traceback.format_exc() - + return Response( { "success" : False, @@ -245,4 +282,4 @@ def MarkNotificationAsViewed(self, request): "error" : error }, status = status.HTTP_500_INTERNAL_SERVER_ERROR - ) \ No newline at end of file + ) From f91702ddebe7915fa6269a7b2d18185de5c5920e Mon Sep 17 00:00:00 2001 From: Vignesh16879 Date: Wed, 4 Sep 2024 17:10:35 +0530 Subject: [PATCH 07/17] 1.5 --- .gitmodules | 2 +- .../__init__.py | 0 .../admin.py | 0 .../apps.py | 0 .../migrations/0001_initial.py | 0 .../migrations/__init__.py | 0 .../models.py | 0 .../permissions.py | 0 .../tests.py | 0 .../urls.py | 0 .../views.py | 0 .../migrations/0003_notifications.py | 53 ++++ cvat/apps/organizations/models.py | 27 ++ cvat/apps/organizations/serializers.py | 8 +- cvat/apps/organizations/urls.py | 3 +- cvat/apps/organizations/views.py | 292 +++++++++++++++++- cvat/settings/base.py | 4 +- 17 files changed, 376 insertions(+), 13 deletions(-) rename cvat/apps/{notifications => notifications23}/__init__.py (100%) rename cvat/apps/{notifications => notifications23}/admin.py (100%) rename cvat/apps/{notifications => notifications23}/apps.py (100%) rename cvat/apps/{notifications => notifications23}/migrations/0001_initial.py (100%) rename cvat/apps/{notifications => notifications23}/migrations/__init__.py (100%) rename cvat/apps/{notifications => notifications23}/models.py (100%) rename cvat/apps/{notifications => notifications23}/permissions.py (100%) rename cvat/apps/{notifications => notifications23}/tests.py (100%) rename cvat/apps/{notifications => notifications23}/urls.py (100%) rename cvat/apps/{notifications => notifications23}/views.py (100%) create mode 100644 cvat/apps/organizations/migrations/0003_notifications.py diff --git a/.gitmodules b/.gitmodules index ae36fe056e5f..61610061386a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "site/themes/docsy"] path = site/themes/docsy - url = https://github.com/google/docsy + url = git@github.com:google/docsy.git diff --git a/cvat/apps/notifications/__init__.py b/cvat/apps/notifications23/__init__.py similarity index 100% rename from cvat/apps/notifications/__init__.py rename to cvat/apps/notifications23/__init__.py diff --git a/cvat/apps/notifications/admin.py b/cvat/apps/notifications23/admin.py similarity index 100% rename from cvat/apps/notifications/admin.py rename to cvat/apps/notifications23/admin.py diff --git a/cvat/apps/notifications/apps.py b/cvat/apps/notifications23/apps.py similarity index 100% rename from cvat/apps/notifications/apps.py rename to cvat/apps/notifications23/apps.py diff --git a/cvat/apps/notifications/migrations/0001_initial.py b/cvat/apps/notifications23/migrations/0001_initial.py similarity index 100% rename from cvat/apps/notifications/migrations/0001_initial.py rename to cvat/apps/notifications23/migrations/0001_initial.py diff --git a/cvat/apps/notifications/migrations/__init__.py b/cvat/apps/notifications23/migrations/__init__.py similarity index 100% rename from cvat/apps/notifications/migrations/__init__.py rename to cvat/apps/notifications23/migrations/__init__.py diff --git a/cvat/apps/notifications/models.py b/cvat/apps/notifications23/models.py similarity index 100% rename from cvat/apps/notifications/models.py rename to cvat/apps/notifications23/models.py diff --git a/cvat/apps/notifications/permissions.py b/cvat/apps/notifications23/permissions.py similarity index 100% rename from cvat/apps/notifications/permissions.py rename to cvat/apps/notifications23/permissions.py diff --git a/cvat/apps/notifications/tests.py b/cvat/apps/notifications23/tests.py similarity index 100% rename from cvat/apps/notifications/tests.py rename to cvat/apps/notifications23/tests.py diff --git a/cvat/apps/notifications/urls.py b/cvat/apps/notifications23/urls.py similarity index 100% rename from cvat/apps/notifications/urls.py rename to cvat/apps/notifications23/urls.py diff --git a/cvat/apps/notifications/views.py b/cvat/apps/notifications23/views.py similarity index 100% rename from cvat/apps/notifications/views.py rename to cvat/apps/notifications23/views.py diff --git a/cvat/apps/organizations/migrations/0003_notifications.py b/cvat/apps/organizations/migrations/0003_notifications.py new file mode 100644 index 000000000000..a6f2fbb48b94 --- /dev/null +++ b/cvat/apps/organizations/migrations/0003_notifications.py @@ -0,0 +1,53 @@ +# Generated by Django 4.2.13 on 2024-09-04 10:20 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("organizations", "0002_invitation_sent_date"), + ] + + operations = [ + migrations.CreateModel( + name="Notifications", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=255)), + ("message", models.TextField()), + ("extra_data", models.JSONField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("is_read", models.BooleanField(default=False)), + ("read_at", models.DateTimeField(blank=True, null=True)), + ( + "notification_type", + models.CharField( + choices=[ + ("info", "Info"), + ("warning", "Warning"), + ("success", "Success"), + ("error", "Error"), + ], + max_length=50, + ), + ), + ( + "recipient", + models.ManyToManyField( + related_name="notifications", to=settings.AUTH_USER_MODEL + ), + ), + ], + ), + ] diff --git a/cvat/apps/organizations/models.py b/cvat/apps/organizations/models.py index 3da77bafbebf..c6ae945e5aa6 100644 --- a/cvat/apps/organizations/models.py +++ b/cvat/apps/organizations/models.py @@ -119,3 +119,30 @@ def accept(self, date=None): class Meta: default_permissions = () + + +## Notifications +from django.db.models import * + +from django.contrib.auth.models import User +# Create your models here. + +class Notifications(Model): + title = CharField(max_length=255) + message = TextField() + extra_data = JSONField(blank=True, null=True) + created_at = DateTimeField(auto_now_add=True) + is_read = BooleanField(default=False) + read_at = DateTimeField(blank=True, null=True) + + recipient = ManyToManyField(User, related_name='notifications') + + notification_type = CharField(max_length=50, choices=[ + ('info', 'Info'), + ('warning', 'Warning'), + ('success', 'Success'), + ('error', 'Error') + ]) + + def __str__(self): + return f"Notification - {self.title}" \ No newline at end of file diff --git a/cvat/apps/organizations/serializers.py b/cvat/apps/organizations/serializers.py index 9cfb467aa3b9..83e5293a594b 100644 --- a/cvat/apps/organizations/serializers.py +++ b/cvat/apps/organizations/serializers.py @@ -14,7 +14,7 @@ from rest_framework import serializers from cvat.apps.engine.serializers import BasicUserSerializer from cvat.apps.iam.utils import get_dummy_user -from .models import Invitation, Membership, Organization +from .models import * class OrganizationReadSerializer(serializers.ModelSerializer): owner = BasicUserSerializer(allow_null=True) @@ -158,3 +158,9 @@ class Meta: class AcceptInvitationReadSerializer(serializers.Serializer): organization_slug = serializers.CharField() + + +class NotificationSerializer(serializers.ModelSerializer): + class Meta: + model = Notifications + fields = ['id', 'title', 'message', 'created_at'] \ No newline at end of file diff --git a/cvat/apps/organizations/urls.py b/cvat/apps/organizations/urls.py index 068f72b0968d..ba9df98bba3d 100644 --- a/cvat/apps/organizations/urls.py +++ b/cvat/apps/organizations/urls.py @@ -3,11 +3,12 @@ # SPDX-License-Identifier: MIT from rest_framework.routers import DefaultRouter -from .views import InvitationViewSet, MembershipViewSet, OrganizationViewSet +from .views import * router = DefaultRouter(trailing_slash=False) router.register('organizations', OrganizationViewSet) router.register('invitations', InvitationViewSet) router.register('memberships', MembershipViewSet) +router.register('notifications', NotificationsViewSet, basename='notifications') urlpatterns = router.urls diff --git a/cvat/apps/organizations/views.py b/cvat/apps/organizations/views.py index 3cd810abce54..4361b48b619c 100644 --- a/cvat/apps/organizations/views.py +++ b/cvat/apps/organizations/views.py @@ -11,6 +11,7 @@ from rest_framework.permissions import SAFE_METHODS from rest_framework.decorators import action from rest_framework.response import Response +from rest_framework.filters import SearchFilter from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_view @@ -22,11 +23,8 @@ from .models import Invitation, Membership, Organization -from .serializers import ( - InvitationReadSerializer, InvitationWriteSerializer, - MembershipReadSerializer, MembershipWriteSerializer, - OrganizationReadSerializer, OrganizationWriteSerializer, - AcceptInvitationReadSerializer) +import traceback +from .serializers import * @extend_schema(tags=['organizations']) @extend_schema_view( @@ -272,11 +270,11 @@ def accept(self, request, pk): invitation.accept() response_serializer = AcceptInvitationReadSerializer(data={'organization_slug': invitation.membership.organization.slug}) response_serializer.is_valid(raise_exception=True) - + ## Send Notification from rest_framework.test import APIRequestFactory from ..notifications.views import NotificationsViewSet - + request_data = { "org": invitation.membership.organization.id, "title": "New member Joined", @@ -290,7 +288,7 @@ def accept(self, request, pk): 'post' : 'SendNotification' }) response = notifications_view(req) - + 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.") @@ -317,3 +315,281 @@ def decline(self, request, pk): return Response(status=status.HTTP_204_NO_CONTENT) except Invitation.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND, data="This invitation does not exist.") + + + + +## Notification View Set +from .models import * + +@extend_schema(tags=['notifications']) +class NotificationsViewSet(viewsets.GenericViewSet, + mixins.RetrieveModelMixin, + mixins.ListModelMixin, + mixins.CreateModelMixin, + mixins.DestroyModelMixin, + PartialUpdateModelMixin, + ): + isAuthorized = True + queryset = Notifications.objects.all() + serializer_class = NotificationSerializer + filter_backends = [SearchFilter] + search_fields = ['title', 'message'] + + + + def AddNotification(self, req): + try: + notification = Notifications.objects.create( + title = req.get('title'), + message = req.get('message'), + notification_type = req.get('notification_type'), + extra_data = req.get('extra_data', {}), + ) + notification.save() + + return Response( + { + "success" : True, + "message" : "Notification saved successfully.", + "data" : { + "notification" : notification + }, + "error" : None + } + ) + except Exception as e: + error = traceback.format_exc() + + return Response( + { + "success" : False, + "message" : "An error occurred while saving notification.", + "data" : {}, + "error" : error + }, + status = status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + @action(detail=False, methods=['post']) + def SendNotification(self, request): + try: + req = request.data + + if "user" in req or "org" in req: + response = self.AddNotification(req) + print(response) + + if not response.data.get("success"): + return response + + notification = response.data.get("data")["notification"] + + if "user" in req: + user = req["user"] + response = self.SendUserNotifications(notification, user, req) + elif "org" in req: + response = self.SendOrganizationNotifications(notification, req) + else: + return Response( + { + "success" : False, + "message" : "Invalid request data. 'user' or 'org' key is required.", + "data" : {}, + "error" : None + }, + status = status.HTTP_400_BAD_REQUEST + ) + + # if response["success"] == False: + # self.try_delete_notification(notification) + + return response + except Exception as e: + error = traceback.format_exc() + + return Response( + { + "success" : False, + "message" : "An error occurred while sending notification.", + "data" : {}, + "error" : error + }, + status = status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + + def SendUserNotifications(self, notification, usr, req): + try: + user = User.objects.get(id=usr) + notification.recipient.add(user) + + return Response( + { + "success" : True, + "message" : "Notification sent successfully.", + "data" : {}, + "error" : None + }, + status = status.HTTP_201_CREATED + ) + except User.DoesNotExist: + return Response( + { + "success" : False, + "message" : f"User with id {usr} does not exist.", + "data" : {}, + "error" : None + }, + status = status.HTTP_404_NOT_FOUND + ) + except Exception as e: + error = traceback.format_exc() + + return Response( + { + "success" : False, + "message" : "An error occurred while sending user notification.", + "data" : {}, + "error" : error + }, + status = status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + + def SendOrganizationNotifications(self, notification, req): + try: + organization = Organization.objects.get(id=req["org"]) + members = organization.members.filter(is_active=True) + errors = [] + + for member in members: + user = member.user + response = self.SendUserNotifications(notification, user.id, req) + + if not response.data.get("success"): + errors.append(f"Error occurred while sending notification to user ({user.username}). Error: {response.data.get('error')}") + + if not errors: + return Response( + { + "success" : True, + "message" : "Notifications sent successfully.", + "data" : {}, + "error" : None + }, + status = status.HTTP_200_OK + ) + else: + return Response( + { + "success" : False, + "message" : "Unable to send notifications to one or more users.", + "data" : {}, + "error" : errors + }, + status = status.HTTP_504_GATEWAY_TIMEOUT + ) + except Organization.DoesNotExist: + return Response( + { + "success" : False, + "message" : f"Organization with id {req['org']} does not exist.", + "data" : {}, + "error" : None + }, + status = status.HTTP_404_NOT_FOUND + ) + except Exception as e: + error = traceback.format_exc() + + return Response( + { + "success" : False, + "message" : "An error occurred while sending organization notifications.", + "data" : {}, + "error" : error + }, + status = status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + @action(detail=False, methods=['post']) + def FetchUserNotifications(self, request): + try: + user = request.user + notifications = Notifications.objects.filter(recipient=User.objects.get(id=user.id)) + data = [] + + for notification in notifications: + noti = { + "title" : notification.title, + "message" : notification.message, + "created_at" : notification.created_at, + "is_read" : notification.is_read, + "notification_type" : notification.notification_type + } + data.append(noti) + + return Response( + { + "success" : True, + "message" : "User notifications fetched successfully.", + "data" : { + "notifications" : data + }, + "error" : None + }, + status = status.HTTP_200_OK + ) + except Exception as e: + error = traceback.format_exc() + + return Response( + { + "success" : False, + "message" : "An error occurred while fetching notifications.", + "data" : {}, + "error" : error + }, + status = status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + @action(detail=False, methods=['post']) + def MarkNotificationAsViewed(self, request): + try: + notification_id = request.data.get('notification_id') + notification = Notifications.objects.get(id=notification_id, recipient=request.user) + notification.is_read = True + notification.read_at = timezone.now() + notification.save() + + return Response( + { + "success" : True, + "message" : "Notification marked as viewed.", + "data" : {}, + "error" : None + }, + status = status.HTTP_200_OK + ) + except Notifications.DoesNotExist: + return Response( + { + "success" : False, + "message" : f"Notification with id {notification_id} does not exist or does not belong to you.", + "data" : {}, + "error" : None + }, + status = status.HTTP_404_NOT_FOUND + ) + except Exception as e: + error = traceback.format_exc() + return Response( + { + "success" : False, + "message" : "An error occurred while marking notification as viewed.", + "data" : {}, + "error" : error + }, + status = status.HTTP_500_INTERNAL_SERVER_ERROR + ) \ No newline at end of file diff --git a/cvat/settings/base.py b/cvat/settings/base.py index 12757f3f8b81..3a57232e1853 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -117,7 +117,7 @@ def generate_secret_key(): 'cvat.apps.events', 'cvat.apps.quality_control', 'cvat.apps.analytics_report', - 'cvat.apps.notifications', + # 'cvat.apps.notifications', ] SITE_ID = 1 @@ -657,7 +657,7 @@ class CVAT_QUEUES(Enum): ACCOUNT_ADAPTER = 'cvat.apps.iam.adapters.DefaultAccountAdapterEx' -CVAT_HOST = os.getenv('CVAT_HOST', 'localhost') +CVAT_HOST = os.getenv('CVAT_HOST', '192.168.0.181') CVAT_BASE_URL = os.getenv('CVAT_BASE_URL', f'http://{CVAT_HOST}:8080').rstrip('/') CLICKHOUSE = { From 050ad7576ae40fbe0d9051c58c41ab2fe9b5623b Mon Sep 17 00:00:00 2001 From: Vignesh16879 Date: Fri, 6 Sep 2024 12:59:44 +0530 Subject: [PATCH 08/17] 1.6 --- cvat/apps/iam/permissions.py | 4 +- .../__init__.py | 0 cvat/apps/notifications/admin.py | 11 + .../apps.py | 0 .../migrations/0001_initial.py | 0 .../migrations/__init__.py | 0 .../models.py | 0 .../permissions.py | 9 +- cvat/apps/notifications/serializers.py | 10 + .../tests.py | 0 .../views.py | 25 +- cvat/apps/notifications23/admin.py | 3 - cvat/apps/notifications23/urls.py | 10 - cvat/apps/organizations/models.py | 29 +- cvat/apps/organizations/serializers.py | 8 +- cvat/apps/organizations/urls.py | 1 - cvat/apps/organizations/views.py | 304 +----------------- cvat/settings/base.py | 9 +- 18 files changed, 53 insertions(+), 370 deletions(-) rename cvat/apps/{notifications23 => notifications}/__init__.py (100%) create mode 100644 cvat/apps/notifications/admin.py rename cvat/apps/{notifications23 => notifications}/apps.py (100%) rename cvat/apps/{notifications23 => notifications}/migrations/0001_initial.py (100%) rename cvat/apps/{notifications23 => notifications}/migrations/__init__.py (100%) rename cvat/apps/{notifications23 => notifications}/models.py (100%) rename cvat/apps/{notifications23 => notifications}/permissions.py (86%) create mode 100644 cvat/apps/notifications/serializers.py rename cvat/apps/{notifications23 => notifications}/tests.py (100%) rename cvat/apps/{notifications23 => notifications}/views.py (93%) delete mode 100644 cvat/apps/notifications23/admin.py delete mode 100644 cvat/apps/notifications23/urls.py diff --git a/cvat/apps/iam/permissions.py b/cvat/apps/iam/permissions.py index b4e802378f96..e499196d364d 100644 --- a/cvat/apps/iam/permissions.py +++ b/cvat/apps/iam/permissions.py @@ -149,7 +149,9 @@ def get_resource(self): def check_access(self) -> PermissionResult: with make_requests_session() as session: response = session.post(self.url, json=self.payload) - output = response.json()['result'] + output = response.json() + print(f"\nOutput: {response}") + output = output['result'] allow = False reasons = [] diff --git a/cvat/apps/notifications23/__init__.py b/cvat/apps/notifications/__init__.py similarity index 100% rename from cvat/apps/notifications23/__init__.py rename to cvat/apps/notifications/__init__.py diff --git a/cvat/apps/notifications/admin.py b/cvat/apps/notifications/admin.py new file mode 100644 index 000000000000..7f2ddccc64af --- /dev/null +++ b/cvat/apps/notifications/admin.py @@ -0,0 +1,11 @@ +from django.contrib import admin + +from .models import * +# Register your models here. + + +class NotificationsAdmin(admin.ModelAdmin): + model = Notifications + + +admin.site.register(Notifications, NotificationsAdmin) \ No newline at end of file diff --git a/cvat/apps/notifications23/apps.py b/cvat/apps/notifications/apps.py similarity index 100% rename from cvat/apps/notifications23/apps.py rename to cvat/apps/notifications/apps.py diff --git a/cvat/apps/notifications23/migrations/0001_initial.py b/cvat/apps/notifications/migrations/0001_initial.py similarity index 100% rename from cvat/apps/notifications23/migrations/0001_initial.py rename to cvat/apps/notifications/migrations/0001_initial.py diff --git a/cvat/apps/notifications23/migrations/__init__.py b/cvat/apps/notifications/migrations/__init__.py similarity index 100% rename from cvat/apps/notifications23/migrations/__init__.py rename to cvat/apps/notifications/migrations/__init__.py diff --git a/cvat/apps/notifications23/models.py b/cvat/apps/notifications/models.py similarity index 100% rename from cvat/apps/notifications23/models.py rename to cvat/apps/notifications/models.py diff --git a/cvat/apps/notifications23/permissions.py b/cvat/apps/notifications/permissions.py similarity index 86% rename from cvat/apps/notifications23/permissions.py rename to cvat/apps/notifications/permissions.py index c4000217c128..6915c6831e6d 100644 --- a/cvat/apps/notifications23/permissions.py +++ b/cvat/apps/notifications/permissions.py @@ -1,5 +1,3 @@ -# notifications/permissions.py - from django.conf import settings from cvat.apps.iam.permissions import OpenPolicyAgentPermission, StrEnum @@ -12,9 +10,11 @@ class Scopes(StrEnum): @classmethod def create(cls, request, view, obj, iam_context): permissions = [] + for scope in cls.get_scopes(request, view, obj): perm = cls.create_base_perm(request, view, scope, iam_context, obj) permissions.append(perm) + return permissions def __init__(self, **kwargs): @@ -30,9 +30,8 @@ def get_scopes(request, view, obj): return [Scopes.VIEW] elif view.action == 'MarkNotificationAsViewed': return [Scopes.MARK_AS_READ] + return [] def get_resource(self): - return { - 'visibility': 'public' if settings.RESTRICTIONS.get('notifications_visibility', True) else 'private', - } + return None \ No newline at end of file diff --git a/cvat/apps/notifications/serializers.py b/cvat/apps/notifications/serializers.py new file mode 100644 index 000000000000..976ffbabf2b0 --- /dev/null +++ b/cvat/apps/notifications/serializers.py @@ -0,0 +1,10 @@ +from rest_framework import serializers + +from .models import * + + +class NotificationsSerializer(serializers.ModelSerializer): + class Meta: + model = Notifications + fields = ['id', 'title', 'message', 'notification_type', 'extra_data', 'created_at', 'is_read', 'read_at'] + read_only_fields = ['id', 'created_at', 'is_read', 'read_at'] \ No newline at end of file diff --git a/cvat/apps/notifications23/tests.py b/cvat/apps/notifications/tests.py similarity index 100% rename from cvat/apps/notifications23/tests.py rename to cvat/apps/notifications/tests.py diff --git a/cvat/apps/notifications23/views.py b/cvat/apps/notifications/views.py similarity index 93% rename from cvat/apps/notifications23/views.py rename to cvat/apps/notifications/views.py index 2c79bd38905f..55d08ede17ab 100644 --- a/cvat/apps/notifications23/views.py +++ b/cvat/apps/notifications/views.py @@ -4,25 +4,13 @@ from rest_framework import status, viewsets from rest_framework.response import Response -import traceback - from .models import * +from .serializers import * + +import traceback # Create your views here. ## Usage -# from rest_framework.test import APIRequestFactory - -# request_data = { -# "user": 1, -# "title": "Test Notification", -# "message": "This is a test notification message.", -# "notification_type": "info", -# "extra_data": {"key": "value"} -# } -# factory = APIRequestFactory() -# req = factory.post('/api/notifications', request_data, format='json') -# notifications_view = NotificationsViewSet.as_view({'post': 'SendNotification'}) -# response = notifications_view(req) class NotificationsViewSet(viewsets.ViewSet): isAuthorized = True @@ -212,11 +200,9 @@ def FetchUserNotifications(self, request): noti = { "title" : notification.title, "message" : notification.message, - "url" : notification.url, "created_at" : notification.created_at, "is_read" : notification.is_read, - "notification_type" : notification.notification_type, - "files" : notification.files.url if notification.files else None, + "notification_type" : notification.notification_type } data.append(noti) @@ -274,6 +260,7 @@ def MarkNotificationAsViewed(self, request): ) except Exception as e: error = traceback.format_exc() + return Response( { "success" : False, @@ -282,4 +269,4 @@ def MarkNotificationAsViewed(self, request): "error" : error }, status = status.HTTP_500_INTERNAL_SERVER_ERROR - ) + ) \ No newline at end of file diff --git a/cvat/apps/notifications23/admin.py b/cvat/apps/notifications23/admin.py deleted file mode 100644 index 8c38f3f3dad5..000000000000 --- a/cvat/apps/notifications23/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/cvat/apps/notifications23/urls.py b/cvat/apps/notifications23/urls.py deleted file mode 100644 index 8ad120bed2cb..000000000000 --- a/cvat/apps/notifications23/urls.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.urls import path, include - -from .views import * - - -urlpatterns = [ - path('api/notifications/', NotificationsViewSet.as_view({'post': 'SendNotification'}), name='send-notification'), - path('api/notifications/fetch/', NotificationsViewSet.as_view({'get': 'FetchUserNotifications'}), name='fetch-notifications'), - path('api/notifications/mark-viewed/', NotificationsViewSet.as_view({'post': 'MarkNotificationAsViewed'}), name='mark-notification-viewed'), -] \ No newline at end of file diff --git a/cvat/apps/organizations/models.py b/cvat/apps/organizations/models.py index c6ae945e5aa6..44c958e4392d 100644 --- a/cvat/apps/organizations/models.py +++ b/cvat/apps/organizations/models.py @@ -118,31 +118,4 @@ def accept(self, date=None): self.membership.save() class Meta: - default_permissions = () - - -## Notifications -from django.db.models import * - -from django.contrib.auth.models import User -# Create your models here. - -class Notifications(Model): - title = CharField(max_length=255) - message = TextField() - extra_data = JSONField(blank=True, null=True) - created_at = DateTimeField(auto_now_add=True) - is_read = BooleanField(default=False) - read_at = DateTimeField(blank=True, null=True) - - recipient = ManyToManyField(User, related_name='notifications') - - notification_type = CharField(max_length=50, choices=[ - ('info', 'Info'), - ('warning', 'Warning'), - ('success', 'Success'), - ('error', 'Error') - ]) - - def __str__(self): - return f"Notification - {self.title}" \ No newline at end of file + default_permissions = () \ No newline at end of file diff --git a/cvat/apps/organizations/serializers.py b/cvat/apps/organizations/serializers.py index 83e5293a594b..3a46b1576050 100644 --- a/cvat/apps/organizations/serializers.py +++ b/cvat/apps/organizations/serializers.py @@ -157,10 +157,4 @@ class Meta: read_only_fields = ['user', 'organization', 'is_active', 'joined_date'] class AcceptInvitationReadSerializer(serializers.Serializer): - organization_slug = serializers.CharField() - - -class NotificationSerializer(serializers.ModelSerializer): - class Meta: - model = Notifications - fields = ['id', 'title', 'message', 'created_at'] \ No newline at end of file + organization_slug = serializers.CharField() \ No newline at end of file diff --git a/cvat/apps/organizations/urls.py b/cvat/apps/organizations/urls.py index ba9df98bba3d..00f38b20aaf3 100644 --- a/cvat/apps/organizations/urls.py +++ b/cvat/apps/organizations/urls.py @@ -9,6 +9,5 @@ router.register('organizations', OrganizationViewSet) router.register('invitations', InvitationViewSet) router.register('memberships', MembershipViewSet) -router.register('notifications', NotificationsViewSet, basename='notifications') urlpatterns = router.urls diff --git a/cvat/apps/organizations/views.py b/cvat/apps/organizations/views.py index 4361b48b619c..7ede2a748dde 100644 --- a/cvat/apps/organizations/views.py +++ b/cvat/apps/organizations/views.py @@ -275,19 +275,19 @@ def accept(self, request, pk): from rest_framework.test import APIRequestFactory from ..notifications.views import NotificationsViewSet - request_data = { - "org": invitation.membership.organization.id, - "title": "New member Joined", - "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) + viewset = NotificationsViewSet() + + request = factory.post('/notifications/', { + 'org' : invitation.membership.organization.id, + 'title': 'Test Notification', + 'message': 'This is a test message', + 'notification_type': 'info', + 'extra_data': {'key': 'value'} + }, format='json') + + response = viewset.SendNotification(request) + return Response(status=status.HTTP_200_OK, data=response_serializer.data) except Invitation.DoesNotExist: @@ -314,282 +314,4 @@ def decline(self, request, pk): membership.delete() return Response(status=status.HTTP_204_NO_CONTENT) except Invitation.DoesNotExist: - return Response(status=status.HTTP_404_NOT_FOUND, data="This invitation does not exist.") - - - - -## Notification View Set -from .models import * - -@extend_schema(tags=['notifications']) -class NotificationsViewSet(viewsets.GenericViewSet, - mixins.RetrieveModelMixin, - mixins.ListModelMixin, - mixins.CreateModelMixin, - mixins.DestroyModelMixin, - PartialUpdateModelMixin, - ): - isAuthorized = True - queryset = Notifications.objects.all() - serializer_class = NotificationSerializer - filter_backends = [SearchFilter] - search_fields = ['title', 'message'] - - - - def AddNotification(self, req): - try: - notification = Notifications.objects.create( - title = req.get('title'), - message = req.get('message'), - notification_type = req.get('notification_type'), - extra_data = req.get('extra_data', {}), - ) - notification.save() - - return Response( - { - "success" : True, - "message" : "Notification saved successfully.", - "data" : { - "notification" : notification - }, - "error" : None - } - ) - except Exception as e: - error = traceback.format_exc() - - return Response( - { - "success" : False, - "message" : "An error occurred while saving notification.", - "data" : {}, - "error" : error - }, - status = status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - @action(detail=False, methods=['post']) - def SendNotification(self, request): - try: - req = request.data - - if "user" in req or "org" in req: - response = self.AddNotification(req) - print(response) - - if not response.data.get("success"): - return response - - notification = response.data.get("data")["notification"] - - if "user" in req: - user = req["user"] - response = self.SendUserNotifications(notification, user, req) - elif "org" in req: - response = self.SendOrganizationNotifications(notification, req) - else: - return Response( - { - "success" : False, - "message" : "Invalid request data. 'user' or 'org' key is required.", - "data" : {}, - "error" : None - }, - status = status.HTTP_400_BAD_REQUEST - ) - - # if response["success"] == False: - # self.try_delete_notification(notification) - - return response - except Exception as e: - error = traceback.format_exc() - - return Response( - { - "success" : False, - "message" : "An error occurred while sending notification.", - "data" : {}, - "error" : error - }, - status = status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - - def SendUserNotifications(self, notification, usr, req): - try: - user = User.objects.get(id=usr) - notification.recipient.add(user) - - return Response( - { - "success" : True, - "message" : "Notification sent successfully.", - "data" : {}, - "error" : None - }, - status = status.HTTP_201_CREATED - ) - except User.DoesNotExist: - return Response( - { - "success" : False, - "message" : f"User with id {usr} does not exist.", - "data" : {}, - "error" : None - }, - status = status.HTTP_404_NOT_FOUND - ) - except Exception as e: - error = traceback.format_exc() - - return Response( - { - "success" : False, - "message" : "An error occurred while sending user notification.", - "data" : {}, - "error" : error - }, - status = status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - - def SendOrganizationNotifications(self, notification, req): - try: - organization = Organization.objects.get(id=req["org"]) - members = organization.members.filter(is_active=True) - errors = [] - - for member in members: - user = member.user - response = self.SendUserNotifications(notification, user.id, req) - - if not response.data.get("success"): - errors.append(f"Error occurred while sending notification to user ({user.username}). Error: {response.data.get('error')}") - - if not errors: - return Response( - { - "success" : True, - "message" : "Notifications sent successfully.", - "data" : {}, - "error" : None - }, - status = status.HTTP_200_OK - ) - else: - return Response( - { - "success" : False, - "message" : "Unable to send notifications to one or more users.", - "data" : {}, - "error" : errors - }, - status = status.HTTP_504_GATEWAY_TIMEOUT - ) - except Organization.DoesNotExist: - return Response( - { - "success" : False, - "message" : f"Organization with id {req['org']} does not exist.", - "data" : {}, - "error" : None - }, - status = status.HTTP_404_NOT_FOUND - ) - except Exception as e: - error = traceback.format_exc() - - return Response( - { - "success" : False, - "message" : "An error occurred while sending organization notifications.", - "data" : {}, - "error" : error - }, - status = status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - @action(detail=False, methods=['post']) - def FetchUserNotifications(self, request): - try: - user = request.user - notifications = Notifications.objects.filter(recipient=User.objects.get(id=user.id)) - data = [] - - for notification in notifications: - noti = { - "title" : notification.title, - "message" : notification.message, - "created_at" : notification.created_at, - "is_read" : notification.is_read, - "notification_type" : notification.notification_type - } - data.append(noti) - - return Response( - { - "success" : True, - "message" : "User notifications fetched successfully.", - "data" : { - "notifications" : data - }, - "error" : None - }, - status = status.HTTP_200_OK - ) - except Exception as e: - error = traceback.format_exc() - - return Response( - { - "success" : False, - "message" : "An error occurred while fetching notifications.", - "data" : {}, - "error" : error - }, - status = status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - @action(detail=False, methods=['post']) - def MarkNotificationAsViewed(self, request): - try: - notification_id = request.data.get('notification_id') - notification = Notifications.objects.get(id=notification_id, recipient=request.user) - notification.is_read = True - notification.read_at = timezone.now() - notification.save() - - return Response( - { - "success" : True, - "message" : "Notification marked as viewed.", - "data" : {}, - "error" : None - }, - status = status.HTTP_200_OK - ) - except Notifications.DoesNotExist: - return Response( - { - "success" : False, - "message" : f"Notification with id {notification_id} does not exist or does not belong to you.", - "data" : {}, - "error" : None - }, - status = status.HTTP_404_NOT_FOUND - ) - except Exception as e: - error = traceback.format_exc() - return Response( - { - "success" : False, - "message" : "An error occurred while marking notification as viewed.", - "data" : {}, - "error" : error - }, - status = status.HTTP_500_INTERNAL_SERVER_ERROR - ) \ No newline at end of file + return Response(status=status.HTTP_404_NOT_FOUND, data="This invitation does not exist.") \ No newline at end of file diff --git a/cvat/settings/base.py b/cvat/settings/base.py index 3a57232e1853..f84333408813 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -38,8 +38,8 @@ # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = str(Path(__file__).parents[2]) -ALLOWED_HOSTS = ["*"] -INTERNAL_IPS = ['127.0.0.1',] +ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',') +INTERNAL_IPS = ['127.0.0.1'] def generate_secret_key(): """ @@ -117,7 +117,6 @@ def generate_secret_key(): 'cvat.apps.events', 'cvat.apps.quality_control', 'cvat.apps.analytics_report', - # 'cvat.apps.notifications', ] SITE_ID = 1 @@ -657,7 +656,7 @@ class CVAT_QUEUES(Enum): ACCOUNT_ADAPTER = 'cvat.apps.iam.adapters.DefaultAccountAdapterEx' -CVAT_HOST = os.getenv('CVAT_HOST', '192.168.0.181') +CVAT_HOST = os.getenv('CVAT_HOST', 'localhost') CVAT_BASE_URL = os.getenv('CVAT_BASE_URL', f'http://{CVAT_HOST}:8080').rstrip('/') CLICKHOUSE = { @@ -724,4 +723,4 @@ class CVAT_QUEUES(Enum): update_started_job_registry_cleanup() CLOUD_DATA_DOWNLOADING_MAX_THREADS_NUMBER = 4 -CLOUD_DATA_DOWNLOADING_NUMBER_OF_FILES_PER_THREAD = 1000 +CLOUD_DATA_DOWNLOADING_NUMBER_OF_FILES_PER_THREAD = 1000 \ No newline at end of file From 6cd1a20c1af7997f98cece8c3b97b0d283068942 Mon Sep 17 00:00:00 2001 From: Vignesh16879 Date: Fri, 6 Sep 2024 14:55:11 +0530 Subject: [PATCH 09/17] 1.6 --- cvat/apps/iam/permissions.py | 1 - cvat/apps/notifications/views.py | 78 ++++++++++++------- .../migrations/0004_delete_notifications.py | 16 ++++ cvat/apps/organizations/views.py | 40 +++++----- cvat/settings/base.py | 1 + datumaro | 2 +- 6 files changed, 89 insertions(+), 49 deletions(-) create mode 100644 cvat/apps/organizations/migrations/0004_delete_notifications.py diff --git a/cvat/apps/iam/permissions.py b/cvat/apps/iam/permissions.py index e499196d364d..d8b0a434b4f9 100644 --- a/cvat/apps/iam/permissions.py +++ b/cvat/apps/iam/permissions.py @@ -150,7 +150,6 @@ def check_access(self) -> PermissionResult: with make_requests_session() as session: response = session.post(self.url, json=self.payload) output = response.json() - print(f"\nOutput: {response}") output = output['result'] allow = False diff --git a/cvat/apps/notifications/views.py b/cvat/apps/notifications/views.py index 55d08ede17ab..0bdec021ce86 100644 --- a/cvat/apps/notifications/views.py +++ b/cvat/apps/notifications/views.py @@ -2,11 +2,13 @@ from django.utils import timezone from rest_framework import status, viewsets +from rest_framework.request import Request from rest_framework.response import Response from .models import * from .serializers import * +import json import traceback # Create your views here. @@ -17,11 +19,11 @@ class NotificationsViewSet(viewsets.ViewSet): def AddNotification(self, req): try: + print("Saving Notifications") notification = Notifications.objects.create( - title = req.get('title'), - message = req.get('message'), - notification_type = req.get('notification_type'), - extra_data = req.get('extra_data', {}), + title = req['title'], + message = req['message'], + notification_type = req['notification_type'] ) notification.save() @@ -32,7 +34,7 @@ def AddNotification(self, req): "data" : { "notification" : notification }, - "error" : error + "error" : None } ) except Exception as e: @@ -49,17 +51,19 @@ def AddNotification(self, req): ) - def SendNotification(self, request): + def SendNotification(self, request: Request): try: - req = request.data + print("Sending...") + body = request.body.decode('utf-8') + req = json.loads(body) if "user" in req or "org" in req: - response = self.AAddNotification(req) + response = self.AddNotification(req) - if not response["success"]: + if not response.data["success"]: return response - notification = response["data"]["notification"] + notification = response.data["data"]["notification"] if "user" in req: user = req["user"] @@ -77,12 +81,14 @@ def SendNotification(self, request): status = status.HTTP_400_BAD_REQUEST ) - if response["success"] == False: - self.try_delete_notification(notification) + if response.data["success"] == False: + pass + # self.try_delete_notification(notification) return response except Exception as e: error = traceback.format_exc() + print(error) return Response( { @@ -95,10 +101,11 @@ def SendNotification(self, request): ) - def SendUserNotifications(self, notification, usr, req): + def SendUserNotifications(self, notification, usr): try: - user = User.objects.get(id=usr) + user = User.objects.get(id = usr) notification.recipient.add(user) + notification.save() return Response( { @@ -141,7 +148,7 @@ def SendOrganizationNotifications(self, notification, req): for member in members: user = member.user - response = self.SendUserNotifications(user.id, req) + response = self.SendUserNotifications(notification, user.id) if not response.data.get("success"): errors.append(f"Error occurred while sending notification to user ({user.username}). Error: {response.data.get('error')}") @@ -178,6 +185,7 @@ def SendOrganizationNotifications(self, notification, req): ) except Exception as e: error = traceback.format_exc() + print(error) return Response( { @@ -190,7 +198,7 @@ def SendOrganizationNotifications(self, notification, req): ) - def FetchUserNotifications(self, request): + def FetchUserNotifications(self, request: Request): try: user = request.user notifications = Notifications.objects.filter(recipient=user) @@ -231,40 +239,52 @@ def FetchUserNotifications(self, request): ) - def MarkNotificationAsViewed(self, request): + def MarkNotificationAsViewed(self, request: Request): try: - notification_id = request.data.get('notification_id') - notification = Notifications.objects.get(id=notification_id, recipient=request.user) - notification.is_read = True - notification.read_at = timezone.now() - notification.save() + notification_ids = request.data.get('notification_ids', []) + + if not isinstance(notification_ids, list): + raise ValueError("Notification IDs should be provided as a list.") + + notifications = Notifications.objects.filter(id__in=notification_ids, recipient=request.user) + updated_count = notifications.update(is_read=True, read_at=timezone.now()) + + if updated_count == 0: + return Response( + { + "success" : False, + "message" : "No notifications found or none belong to you.", + "data" : {}, + "error" : None + }, + status=status.HTTP_404_NOT_FOUND + ) return Response( { "success" : True, - "message" : "Notification marked as viewed.", + "message" : f"{updated_count} notifications marked as viewed.", "data" : {}, "error" : None }, - status = status.HTTP_200_OK + status=status.HTTP_200_OK ) - except Notifications.DoesNotExist: + except ValueError as ve: return Response( { "success" : False, - "message" : f"Notification with id {notification_id} does not exist or does not belong to you.", + "message" : str(ve), "data" : {}, "error" : None }, - status = status.HTTP_404_NOT_FOUND + status=status.HTTP_400_BAD_REQUEST ) except Exception as e: error = traceback.format_exc() - return Response( { "success" : False, - "message" : "An error occurred while marking notification as viewed.", + "message" : "An error occurred while marking notifications as viewed.", "data" : {}, "error" : error }, diff --git a/cvat/apps/organizations/migrations/0004_delete_notifications.py b/cvat/apps/organizations/migrations/0004_delete_notifications.py new file mode 100644 index 000000000000..d3b73a68abdd --- /dev/null +++ b/cvat/apps/organizations/migrations/0004_delete_notifications.py @@ -0,0 +1,16 @@ +# Generated by Django 4.2.14 on 2024-09-06 07:55 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("organizations", "0003_notifications"), + ] + + operations = [ + migrations.DeleteModel( + name="Notifications", + ), + ] diff --git a/cvat/apps/organizations/views.py b/cvat/apps/organizations/views.py index 7ede2a748dde..7f0430dc66a3 100644 --- a/cvat/apps/organizations/views.py +++ b/cvat/apps/organizations/views.py @@ -237,6 +237,7 @@ def get_queryset(self): def create(self, request): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) + try: self.perform_create(serializer) except ImproperlyConfigured: @@ -252,7 +253,27 @@ def perform_create(self, serializer): request=self.request, ) + ## Send Notification + print("Calling Notification API") + from rest_framework.test import APIRequestFactory + from ..notifications.views import NotificationsViewSet + + factory = APIRequestFactory() + viewset = NotificationsViewSet() + + request = factory.post('/notifications/', { + 'org' : self.request.iam_context['organization'].id, + 'title': 'Test Notification', + 'message': 'This is a test message', + 'notification_type': 'info', + 'extra_data': {'key': 'value'} + }, format='json') + + response = viewset.SendNotification(request) + print(response) + def perform_update(self, serializer): + if 'accepted' in self.request.query_params: serializer.instance.accept() else: @@ -263,6 +284,7 @@ def perform_update(self, serializer): def accept(self, request, pk): try: invitation = self.get_object() # force to call check_object_permissions + if invitation.expired: return Response(status=status.HTTP_400_BAD_REQUEST, data="Your invitation is expired. Please contact organization owner to renew it.") if invitation.membership.is_active: @@ -271,24 +293,6 @@ def accept(self, request, pk): response_serializer = AcceptInvitationReadSerializer(data={'organization_slug': invitation.membership.organization.slug}) response_serializer.is_valid(raise_exception=True) - ## Send Notification - from rest_framework.test import APIRequestFactory - from ..notifications.views import NotificationsViewSet - - factory = APIRequestFactory() - viewset = NotificationsViewSet() - - request = factory.post('/notifications/', { - 'org' : invitation.membership.organization.id, - 'title': 'Test Notification', - 'message': 'This is a test message', - 'notification_type': 'info', - 'extra_data': {'key': 'value'} - }, format='json') - - response = viewset.SendNotification(request) - - 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/settings/base.py b/cvat/settings/base.py index f84333408813..72cbf0d3c4fd 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -117,6 +117,7 @@ def generate_secret_key(): 'cvat.apps.events', 'cvat.apps.quality_control', 'cvat.apps.analytics_report', + 'cvat.apps.notifications', ] SITE_ID = 1 diff --git a/datumaro b/datumaro index 125840fc6b28..393cb6665290 160000 --- a/datumaro +++ b/datumaro @@ -1 +1 @@ -Subproject commit 125840fc6b28875cce4c85626a5c36bb9e0d2a83 +Subproject commit 393cb666529067060ff57e30cb6e448669274f35 From 9fda5a2a5c6c488419d52b39da0697fc6a21e3e4 Mon Sep 17 00:00:00 2001 From: Vignesh16879 Date: Fri, 6 Sep 2024 16:16:18 +0530 Subject: [PATCH 10/17] 1.7 --- cvat/apps/notifications/admin.py | 9 +- .../migrations/0002_alter_notifications_id.py | 18 + ...3_remove_notifications_is_read_and_more.py | 61 +++ cvat/apps/notifications/models.py | 22 +- cvat/apps/notifications/serializers.py | 46 ++- cvat/apps/notifications/views.py | 347 +++++++++--------- cvat/apps/organizations/views.py | 55 ++- 7 files changed, 362 insertions(+), 196 deletions(-) create mode 100644 cvat/apps/notifications/migrations/0002_alter_notifications_id.py create mode 100644 cvat/apps/notifications/migrations/0003_remove_notifications_is_read_and_more.py diff --git a/cvat/apps/notifications/admin.py b/cvat/apps/notifications/admin.py index 7f2ddccc64af..eb02673b112b 100644 --- a/cvat/apps/notifications/admin.py +++ b/cvat/apps/notifications/admin.py @@ -8,4 +8,11 @@ class NotificationsAdmin(admin.ModelAdmin): model = Notifications -admin.site.register(Notifications, NotificationsAdmin) \ No newline at end of file +admin.site.register(Notifications, NotificationsAdmin) + + +class NotificationStatusAdmin(admin.ModelAdmin): + model = NotificationStatus + + +admin.site.register(NotificationStatus, NotificationStatusAdmin) \ No newline at end of file diff --git a/cvat/apps/notifications/migrations/0002_alter_notifications_id.py b/cvat/apps/notifications/migrations/0002_alter_notifications_id.py new file mode 100644 index 000000000000..000fc93b1248 --- /dev/null +++ b/cvat/apps/notifications/migrations/0002_alter_notifications_id.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.14 on 2024-09-06 09:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("notifications", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="notifications", + name="id", + field=models.AutoField(primary_key=True, serialize=False), + ), + ] diff --git a/cvat/apps/notifications/migrations/0003_remove_notifications_is_read_and_more.py b/cvat/apps/notifications/migrations/0003_remove_notifications_is_read_and_more.py new file mode 100644 index 000000000000..e9975f3a3b5a --- /dev/null +++ b/cvat/apps/notifications/migrations/0003_remove_notifications_is_read_and_more.py @@ -0,0 +1,61 @@ +# Generated by Django 4.2.14 on 2024-09-06 10:07 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("notifications", "0002_alter_notifications_id"), + ] + + operations = [ + migrations.RemoveField( + model_name="notifications", + name="is_read", + ), + migrations.RemoveField( + model_name="notifications", + name="read_at", + ), + migrations.RemoveField( + model_name="notifications", + name="recipient", + ), + migrations.CreateModel( + name="NotificationStatus", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("is_read", models.BooleanField(default=False)), + ("read_at", models.DateTimeField(blank=True, null=True)), + ( + "notification", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="notifications.notifications", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "unique_together": {("notification", "user")}, + }, + ), + ] diff --git a/cvat/apps/notifications/models.py b/cvat/apps/notifications/models.py index 6c09f675c260..9113d64d5d8f 100644 --- a/cvat/apps/notifications/models.py +++ b/cvat/apps/notifications/models.py @@ -4,16 +4,13 @@ from ..organizations.models import * # Create your models here. + class Notifications(Model): + id = AutoField(primary_key=True) title = CharField(max_length=255) message = TextField() extra_data = JSONField(blank=True, null=True) created_at = DateTimeField(auto_now_add=True) - is_read = BooleanField(default=False) - read_at = DateTimeField(blank=True, null=True) - - recipient = ManyToManyField(User, related_name='notifications') - notification_type = CharField(max_length=50, choices=[ ('info', 'Info'), ('warning', 'Warning'), @@ -22,4 +19,17 @@ class Notifications(Model): ]) def __str__(self): - return f"Notification - {self.title}" \ No newline at end of file + return f"Notification - {self.title}" + + +class NotificationStatus(Model): + notification = ForeignKey(Notifications, on_delete=CASCADE) + user = ForeignKey(User, on_delete=CASCADE) + is_read = BooleanField(default=False) + read_at = DateTimeField(blank=True, null=True) + + class Meta: + unique_together = ('notification', 'user') + + def __str__(self): + return f"Status for {self.user.username} - {self.notification.title}" \ No newline at end of file diff --git a/cvat/apps/notifications/serializers.py b/cvat/apps/notifications/serializers.py index 976ffbabf2b0..6b824284a348 100644 --- a/cvat/apps/notifications/serializers.py +++ b/cvat/apps/notifications/serializers.py @@ -1,10 +1,48 @@ +# serializers.py from rest_framework import serializers - from .models import * -class NotificationsSerializer(serializers.ModelSerializer): +class NotificationSerializer(serializers.ModelSerializer): + class Meta: + model = Notifications + fields = ['id', 'title', 'message', 'notification_type', 'created_at'] + + +class UserNotificationStatusSerializer(serializers.ModelSerializer): + class Meta: + model = NotificationStatus + fields = ['is_read', 'read_at'] + + +class UserNotificationDetailSerializer(serializers.ModelSerializer): + status = UserNotificationStatusSerializer(source='notificationstatus_set.first') + class Meta: model = Notifications - fields = ['id', 'title', 'message', 'notification_type', 'extra_data', 'created_at', 'is_read', 'read_at'] - read_only_fields = ['id', 'created_at', 'is_read', 'read_at'] \ No newline at end of file + fields = ['id', 'title', 'message', 'notification_type', 'created_at', 'status'] + + +class AddNotificationSerializer(serializers.Serializer): + title = serializers.CharField(max_length=255) + message = serializers.CharField() + notification_type = serializers.CharField(max_length=50) + + +class SendNotificationSerializer(serializers.Serializer): + user = serializers.IntegerField(required=False) + org = serializers.IntegerField(required=False) + title = serializers.CharField() + message = serializers.CharField() + notification_type = serializers.CharField(max_length=50) + + +class MarkNotificationAsViewedSerializer(serializers.Serializer): + user = serializers.IntegerField() + notification_ids = serializers.ListField( + child=serializers.IntegerField() + ) + + +class FetchUserNotificationsSerializer(serializers.Serializer): + user = serializers.IntegerField() \ No newline at end of file diff --git a/cvat/apps/notifications/views.py b/cvat/apps/notifications/views.py index 0bdec021ce86..1ec475b356cc 100644 --- a/cvat/apps/notifications/views.py +++ b/cvat/apps/notifications/views.py @@ -16,277 +16,272 @@ class NotificationsViewSet(viewsets.ViewSet): isAuthorized = True - - def AddNotification(self, req): - try: - print("Saving Notifications") - notification = Notifications.objects.create( - title = req['title'], - message = req['message'], - notification_type = req['notification_type'] - ) - notification.save() - - return Response( - { - "success" : True, - "message" : "An error occurred while saving notification.", - "data" : { - "notification" : notification + def AddNotification(self, data): + serializer = AddNotificationSerializer(data=data) + if serializer.is_valid(): + try: + notification = Notifications.objects.create( + title=serializer.validated_data['title'], + message=serializer.validated_data['message'], + notification_type=serializer.validated_data['notification_type'] + ) + return Response( + { + "success": True, + "message": "Notification saved successfully.", + "data": { + "notification": UserNotificationDetailSerializer(notification).data + }, + "error": None + } + ) + except Exception as e: + error = traceback.format_exc() + return Response( + { + "success": False, + "message": "An error occurred while saving notification.", + "data": {}, + "error": error }, - "error" : None - } - ) - except Exception as e: - error = traceback.format_exc() - + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + else: return Response( { - "success" : False, - "message" : "An error occurred while saving notification.", - "data" : {}, - "error" : error + "success": False, + "message": "Invalid data.", + "data": serializer.errors, + "error": None }, - status = status.HTTP_500_INTERNAL_SERVER_ERROR + status=status.HTTP_400_BAD_REQUEST ) - def SendNotification(self, request: Request): try: - print("Sending...") - body = request.body.decode('utf-8') - req = json.loads(body) - - if "user" in req or "org" in req: - response = self.AddNotification(req) - + data = request.data # Use request.data instead of json.loads(request.body) + serializer = SendNotificationSerializer(data=data) + if serializer.is_valid(): + response = self.AddNotification(serializer.validated_data) if not response.data["success"]: return response notification = response.data["data"]["notification"] - if "user" in req: - user = req["user"] - response = self.SendUserNotifications(notification, user, req) - elif "org" in req: - response = self.SendOrganizationNotifications(notification, req) + if "user" in serializer.validated_data: + user = serializer.validated_data["user"] + response = self.SendUserNotifications(notification, user) + elif "org" in serializer.validated_data: + response = self.SendOrganizationNotifications(notification, serializer.validated_data) + + return response else: return Response( { - "success" : False, - "message" : "Invalid request data. 'user' or 'org' key is required.", - "data" : {}, - "error" : None + "success": False, + "message": "Invalid request data.", + "data": serializer.errors, + "error": None }, - status = status.HTTP_400_BAD_REQUEST + status=status.HTTP_400_BAD_REQUEST ) - - if response.data["success"] == False: - pass - # self.try_delete_notification(notification) - - return response except Exception as e: error = traceback.format_exc() - print(error) - return Response( { - "success" : False, - "message" : "An error occurred while sending notification.", - "data" : {}, - "error" : error + "success": False, + "message": "An error occurred while sending notification.", + "data": {}, + "error": error }, - status = status.HTTP_500_INTERNAL_SERVER_ERROR + status=status.HTTP_500_INTERNAL_SERVER_ERROR ) - - def SendUserNotifications(self, notification, usr): + def SendUserNotifications(self, notification, user_id): try: - user = User.objects.get(id = usr) - notification.recipient.add(user) - notification.save() - + user = User.objects.get(id=user_id) + notification = Notifications.objects.get(id=notification.get("id")) + NotificationStatus.objects.get_or_create( + notification=notification, + user=user, + defaults={'is_read': False} + ) return Response( { - "success" : True, - "message" : "Notification sent successfully.", - "data" : {}, - "error" : None + "success": True, + "message": "Notification sent successfully.", + "data": {}, + "error": None }, - status = status.HTTP_201_CREATED + status=status.HTTP_201_CREATED ) except User.DoesNotExist: return Response( { - "success" : False, - "message" : f"User with id {usr} does not exist.", - "data" : {}, - "error" : None + "success": False, + "message": f"User with id {user_id} does not exist.", + "data": {}, + "error": None }, - status = status.HTTP_404_NOT_FOUND + status=status.HTTP_404_NOT_FOUND ) except Exception as e: error = traceback.format_exc() - return Response( { - "success" : False, - "message" : "An error occurred while sending user notification.", - "data" : {}, - "error" : error + "success": False, + "message": "An error occurred while sending user notification.", + "data": {}, + "error": error }, - status = status.HTTP_500_INTERNAL_SERVER_ERROR + status=status.HTTP_500_INTERNAL_SERVER_ERROR ) - - def SendOrganizationNotifications(self, notification, req): + def SendOrganizationNotifications(self, notification, data): try: - organization = Organization.objects.get(id=req["org"]) + organization = Organization.objects.get(id=data["org"]) members = organization.members.filter(is_active=True) errors = [] for member in members: user = member.user response = self.SendUserNotifications(notification, user.id) - if not response.data.get("success"): errors.append(f"Error occurred while sending notification to user ({user.username}). Error: {response.data.get('error')}") if not errors: return Response( { - "success" : True, - "message" : "Notifications sent successfully.", - "data" : {}, - "error" : None + "success": True, + "message": "Notifications sent successfully.", + "data": {}, + "error": None }, - status = status.HTTP_200_OK + status=status.HTTP_200_OK ) else: return Response( { - "success" : False, - "message" : "Unable to send notifications to one or more users.", - "data" : {}, - "error" : errors + "success": False, + "message": "Unable to send notifications to one or more users.", + "data": {}, + "error": errors }, - status = status.HTTP_504_GATEWAY_TIMEOUT + status=status.HTTP_504_GATEWAY_TIMEOUT ) except Organization.DoesNotExist: return Response( { - "success" : False, - "message" : f"Organization with id {req['org']} does not exist.", - "data" : {}, - "error" : None + "success": False, + "message": f"Organization with id {data['org']} does not exist.", + "data": {}, + "error": None }, - status = status.HTTP_404_NOT_FOUND + status=status.HTTP_404_NOT_FOUND ) except Exception as e: error = traceback.format_exc() - print(error) - return Response( { - "success" : False, - "message" : "An error occurred while sending organization notifications.", - "data" : {}, - "error" : error + "success": False, + "message": "An error occurred while sending organization notifications.", + "data": {}, + "error": error }, - status = status.HTTP_500_INTERNAL_SERVER_ERROR + status=status.HTTP_500_INTERNAL_SERVER_ERROR ) - def FetchUserNotifications(self, request: Request): try: - user = request.user - notifications = Notifications.objects.filter(recipient=user) - data = [] + data = request.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] - for notification in notifications: - noti = { - "title" : notification.title, - "message" : notification.message, - "created_at" : notification.created_at, - "is_read" : notification.is_read, - "notification_type" : notification.notification_type - } - data.append(noti) - - return Response( - { - "success" : True, - "message" : "User notifications fetched successfully.", - "data" : { - "notifications" : data + return Response( + { + "success": True, + "message": "User notifications fetched successfully.", + "data": { + "notifications": data + }, + "error": None }, - "error" : None - }, - status = status.HTTP_200_OK - ) + status=status.HTTP_200_OK + ) + else: + return Response( + { + "success": False, + "message": "Invalid request data.", + "data": serializer.errors, + "error": None + }, + status=status.HTTP_400_BAD_REQUEST + ) except Exception as e: error = traceback.format_exc() - return Response( { - "success" : False, - "message" : "An error occurred while fetching notifications.", - "data" : {}, - "error" : error + "success": False, + "message": "An error occurred while fetching notifications.", + "data": {}, + "error": error }, - status = status.HTTP_500_INTERNAL_SERVER_ERROR + status=status.HTTP_500_INTERNAL_SERVER_ERROR ) - def MarkNotificationAsViewed(self, request: Request): try: - notification_ids = request.data.get('notification_ids', []) - - if not isinstance(notification_ids, list): - raise ValueError("Notification IDs should be provided as a list.") + data = request.data # Use request.data instead of json.loads(request.body) + serializer = MarkNotificationAsViewedSerializer(data=data) + if serializer.is_valid(): + user_id = serializer.validated_data["user"] + notification_ids = serializer.validated_data["notification_ids"] + + notifications_status = NotificationStatus.objects.filter(notification_id__in=notification_ids, user_id=user_id) + updated_count = notifications_status.update(is_read=True, read_at=timezone.now()) + + if updated_count == 0: + return Response( + { + "success": False, + "message": "No notifications found or none belong to you.", + "data": {}, + "error": None + }, + status=status.HTTP_404_NOT_FOUND + ) - notifications = Notifications.objects.filter(id__in=notification_ids, recipient=request.user) - updated_count = notifications.update(is_read=True, read_at=timezone.now()) - - if updated_count == 0: return Response( { - "success" : False, - "message" : "No notifications found or none belong to you.", - "data" : {}, - "error" : None + "success": True, + "message": f"{updated_count} notifications marked as viewed.", + "data": {}, + "error": None }, - status=status.HTTP_404_NOT_FOUND + status=status.HTTP_200_OK + ) + else: + return Response( + { + "success": False, + "message": "Invalid data.", + "data": serializer.errors, + "error": None + }, + status=status.HTTP_400_BAD_REQUEST ) - - return Response( - { - "success" : True, - "message" : f"{updated_count} notifications marked as viewed.", - "data" : {}, - "error" : None - }, - status=status.HTTP_200_OK - ) - except ValueError as ve: - return Response( - { - "success" : False, - "message" : str(ve), - "data" : {}, - "error" : None - }, - status=status.HTTP_400_BAD_REQUEST - ) except Exception as e: error = traceback.format_exc() return Response( { - "success" : False, - "message" : "An error occurred while marking notifications as viewed.", - "data" : {}, - "error" : error + "success": False, + "message": "An error occurred while marking notifications as viewed.", + "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 7f0430dc66a3..d65327623f46 100644 --- a/cvat/apps/organizations/views.py +++ b/cvat/apps/organizations/views.py @@ -254,23 +254,60 @@ def perform_create(self, serializer): ) ## Send Notification - print("Calling Notification API") - from rest_framework.test import APIRequestFactory from ..notifications.views import NotificationsViewSet + from ..notifications.serializers import (SendNotificationSerializer, FetchUserNotificationsSerializer, MarkNotificationAsViewedSerializer) - factory = APIRequestFactory() viewset = NotificationsViewSet() - request = factory.post('/notifications/', { - 'org' : self.request.iam_context['organization'].id, + # 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', - 'extra_data': {'key': 'value'} - }, format='json') + } + 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}) + ) - response = viewset.SendNotification(request) - print(response) def perform_update(self, serializer): From e3bf29f50513a94b9ec797b6914d420c1eb886bd Mon Sep 17 00:00:00 2001 From: Vignesh16879 Date: Wed, 11 Sep 2024 15:24:50 +0530 Subject: [PATCH 11/17] 1.7 --- cvat/apps/engine/models.py | 1 - cvat/apps/engine/views.py | 29 +++--- cvat/apps/iam/permissions.py | 18 ++-- cvat/apps/notifications/api.py | 126 +++++++++++++++++++++++++ cvat/apps/notifications/serializers.py | 4 +- cvat/apps/notifications/urls.py | 21 +++++ cvat/apps/notifications/views.py | 90 +++++++++++++----- cvat/apps/organizations/views.py | 63 ++----------- cvat/urls.py | 4 +- 9 files changed, 248 insertions(+), 108 deletions(-) create mode 100644 cvat/apps/notifications/api.py create mode 100644 cvat/apps/notifications/urls.py 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 From 61f404640c17d8a9a330e57ecdecac11b387dd57 Mon Sep 17 00:00:00 2001 From: Vignesh16879 Date: Wed, 11 Sep 2024 15:48:36 +0530 Subject: [PATCH 12/17] notifications --- cvat/apps/iam/permissions.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/cvat/apps/iam/permissions.py b/cvat/apps/iam/permissions.py index 5ed0c706f924..d8b0a434b4f9 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 = True + allow = False 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) From 166ba26a1c12c9e1be0b1dd2710297126cbe1c09 Mon Sep 17 00:00:00 2001 From: Vignesh16879 Date: Wed, 11 Sep 2024 15:55:47 +0530 Subject: [PATCH 13/17] notifications --- .gitmodules | 2 +- cvat/apps/iam/permissions.py | 3 +-- cvat/apps/organizations/serializers.py | 2 +- cvat/apps/organizations/urls.py | 2 +- cvat/apps/organizations/views.py | 6 +++++- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.gitmodules b/.gitmodules index 61610061386a..ae36fe056e5f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "site/themes/docsy"] path = site/themes/docsy - url = git@github.com:google/docsy.git + url = https://github.com/google/docsy diff --git a/cvat/apps/iam/permissions.py b/cvat/apps/iam/permissions.py index d8b0a434b4f9..b4e802378f96 100644 --- a/cvat/apps/iam/permissions.py +++ b/cvat/apps/iam/permissions.py @@ -149,8 +149,7 @@ def get_resource(self): 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 = response.json()['result'] allow = False reasons = [] diff --git a/cvat/apps/organizations/serializers.py b/cvat/apps/organizations/serializers.py index 3a46b1576050..966c28c5fa1d 100644 --- a/cvat/apps/organizations/serializers.py +++ b/cvat/apps/organizations/serializers.py @@ -14,7 +14,7 @@ from rest_framework import serializers from cvat.apps.engine.serializers import BasicUserSerializer from cvat.apps.iam.utils import get_dummy_user -from .models import * +from .models import Invitation, Membership, Organization class OrganizationReadSerializer(serializers.ModelSerializer): owner = BasicUserSerializer(allow_null=True) diff --git a/cvat/apps/organizations/urls.py b/cvat/apps/organizations/urls.py index 00f38b20aaf3..068f72b0968d 100644 --- a/cvat/apps/organizations/urls.py +++ b/cvat/apps/organizations/urls.py @@ -3,7 +3,7 @@ # SPDX-License-Identifier: MIT from rest_framework.routers import DefaultRouter -from .views import * +from .views import InvitationViewSet, MembershipViewSet, OrganizationViewSet router = DefaultRouter(trailing_slash=False) router.register('organizations', OrganizationViewSet) diff --git a/cvat/apps/organizations/views.py b/cvat/apps/organizations/views.py index 2b6e4e67e7d8..41be1fdf9d27 100644 --- a/cvat/apps/organizations/views.py +++ b/cvat/apps/organizations/views.py @@ -24,7 +24,11 @@ from .models import Invitation, Membership, Organization import traceback -from .serializers import * +from .serializers import ( + InvitationReadSerializer, InvitationWriteSerializer, + MembershipReadSerializer, MembershipWriteSerializer, + OrganizationReadSerializer, OrganizationWriteSerializer, + AcceptInvitationReadSerializer) @extend_schema(tags=['organizations']) @extend_schema_view( From 01bf22d9fe66a7174af953a34c94823e21613b44 Mon Sep 17 00:00:00 2001 From: Vignesh16879 Date: Wed, 11 Sep 2024 15:57:16 +0530 Subject: [PATCH 14/17] notifications --- cvat/apps/organizations/views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/cvat/apps/organizations/views.py b/cvat/apps/organizations/views.py index 41be1fdf9d27..218bf6db8a15 100644 --- a/cvat/apps/organizations/views.py +++ b/cvat/apps/organizations/views.py @@ -11,7 +11,6 @@ from rest_framework.permissions import SAFE_METHODS from rest_framework.decorators import action from rest_framework.response import Response -from rest_framework.filters import SearchFilter from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_view @@ -23,7 +22,6 @@ from .models import Invitation, Membership, Organization -import traceback from .serializers import ( InvitationReadSerializer, InvitationWriteSerializer, MembershipReadSerializer, MembershipWriteSerializer, From 4f9172398600d952e879794cbb74a94d2dbc4473 Mon Sep 17 00:00:00 2001 From: Vignesh16879 Date: Mon, 11 Nov 2024 06:48:17 +0530 Subject: [PATCH 15/17] added cloud storage for task creation and video chunk creator --- cvat/apps/engine/chunks.py | 95 ++++++++++++++++++++++++++++++++++++ cvat/apps/engine/task.py | 58 +++++++++++++++++++++- cvat/apps/iam/permissions.py | 18 +++---- 3 files changed, 160 insertions(+), 11 deletions(-) create mode 100755 cvat/apps/engine/chunks.py diff --git a/cvat/apps/engine/chunks.py b/cvat/apps/engine/chunks.py new file mode 100755 index 000000000000..230331230cab --- /dev/null +++ b/cvat/apps/engine/chunks.py @@ -0,0 +1,95 @@ +import os +import traceback +import subprocess + + +def get_video_duration(video_file): + result = subprocess.run( + ['ffprobe', '-v', 'error', '-show_entries', 'format=duration', '-of', 'default=noprint_wrappers=1:nokey=1', video_file], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + duration = float(result.stdout) + return duration + + +class MakeVideoChunks: + def make(task_id, chunk_duration=1): + try: + current_file_path = os.path.abspath(__file__) + print(f"Current file path: {current_file_path}") + + # Define the raw video directory + raw_video_dir = f"/home/vignesh/Desktop/Desktop/IIITD/BTP.02/cvat/data/data/{task_id}/raw" + print(f"Raw video directory: {raw_video_dir}") + + # Recursively search for .mp4 files in the raw video directory and its subdirectories + input_files = [] + for root, dirs, files in os.walk(raw_video_dir): + for file in files: + if file.endswith('.mp4'): + input_files.append(os.path.join(root, file)) + + # Check if any .mp4 files are found + if not input_files: + raise FileNotFoundError("No .mp4 files found in the specified directory or subdirectories.") + + print(f"Input files: {input_files}") + input_file = input_files[0] # Use the first .mp4 file found + output_folder = f"/home/vignesh/Desktop/Desktop/IIITD/BTP.02/cvat/data/data/{task_id}/compressed" + + # Create the output folder if it doesn't exist + os.makedirs(output_folder, exist_ok=True) + + print(f"Processing video: {input_file}") + + # Retrieve video duration + video_duration = get_video_duration(input_file) + print(f"Video duration: {video_duration} seconds") + + # Define start and end times + start_time = 0 # Start from the beginning of the video + end_time = int(video_duration) # Set end time to the duration of the video + + # Create chunks using a loop + for i in range(start_time, end_time, chunk_duration): + output_file = os.path.join(output_folder, f'{i}.mp4') + + # If the output file exists, remove it + if os.path.exists(output_file): + print(f"File {output_file} already exists. Removing it.") + os.remove(output_file) + + command = [ + 'ffmpeg', + '-ss', str(i), # Start time for the chunk + '-i', input_file, # Input file + '-c', 'copy', # Copy codec, no re-encoding + '-t', str(chunk_duration), # Duration of the chunk + output_file # Output file path + ] + + # Execute the command + print(' '.join(command)) + subprocess.run(command) + + response = { + "success": True, + "message": None, + "data": None, + "error": None + } + + return response + except Exception as e: + print(str(e)) + error = traceback.print_exc() + + response = { + "success": False, + "message": f"An unexpected error occurred, Error: {e}", + "data": None, + "error": error + } + + return response \ No newline at end of file diff --git a/cvat/apps/engine/task.py b/cvat/apps/engine/task.py index 4fe4c4b46039..ba228955c4dc 100644 --- a/cvat/apps/engine/task.py +++ b/cvat/apps/engine/task.py @@ -39,6 +39,7 @@ from utils.dataset_manifest.core import VideoManifestValidator, is_dataset_manifest from utils.dataset_manifest.utils import detect_related_images from .cloud_provider import db_storage_to_storage_instance +from .chunks import MakeVideoChunks slogger = ServerLogManager(__name__) @@ -105,6 +106,7 @@ def _copy_data_from_share_point( )) for path in filtered_server_files: + slogger.glob.info(f"Copying file: {path}") if server_dir is None: source_path = os.path.join(settings.SHARE_ROOT, os.path.normpath(path)) else: @@ -449,8 +451,10 @@ def _download_data_from_cloud_storage( files: List[str], upload_dir: str, ): + slogger.glob.info(f"Downloading data from cloud storage: {files}") cloud_storage_instance = db_storage_to_storage_instance(db_storage) cloud_storage_instance.bulk_download_to_dir(files, upload_dir) + slogger.glob.info(f"Downloaded data to {upload_dir}") def _get_manifest_frame_indexer(start_frame=0, frame_step=1): return lambda frame_id: start_frame + frame_id * frame_step @@ -559,6 +563,7 @@ def _create_thread( slogger.glob.info("create task #{}".format(db_task.id)) job_file_mapping = _validate_job_file_mapping(db_task, data) + slogger.glob.info(f"Job file mapping: {job_file_mapping}") db_data = db_task.data upload_dir = db_data.get_upload_dirname() if db_data.storage != models.StorageChoice.SHARE else settings.SHARE_ROOT @@ -700,24 +705,29 @@ def _update_status(msg: str) -> None: # count and validate uploaded files media = _count_files(data) + slogger.glob.info(f"Media: {media}") media, task_mode = _validate_data(media, manifest_files) is_media_sorted = False if is_data_in_cloud: # first we need to filter files and keep only supported ones + slogger.glob.info(f"Data in cloud") if any([v for k, v in media.items() if k != 'image']) and db_data.storage_method == models.StorageMethodChoice.CACHE: + slogger.glob.info(f"Storage method: {db_data.storage_method}") # FUTURE-FIXME: This is a temporary workaround for creating tasks # with unsupported cloud storage data (video, archive, pdf) when use_cache is enabled db_data.storage_method = models.StorageMethodChoice.FILE_SYSTEM - _update_status("The 'use cache' option is ignored") + # _update_status("The 'use cache' option is ignored") if db_data.storage_method == models.StorageMethodChoice.FILE_SYSTEM or not settings.USE_CACHE: + slogger.glob.info(f"Storage method: {db_data.storage_method}") filtered_data = [] for files in (i for i in media.values() if i): filtered_data.extend(files) media_to_download = filtered_data - if media['image']: + if 'image' in media and media['image']: + slogger.glob.info(f"Image in media") start_frame = db_data.start_frame stop_frame = len(filtered_data) - 1 if data['stop_frame'] is not None: @@ -726,40 +736,62 @@ def _update_status(msg: str) -> None: step = db_data.get_frame_step() if start_frame or step != 1 or stop_frame != len(filtered_data) - 1: media_to_download = filtered_data[start_frame : stop_frame + 1: step] + + slogger.glob.info(f"Downloading data from cloud storage: {media_to_download}") _download_data_from_cloud_storage(db_data.cloud_storage, media_to_download, upload_dir) del media_to_download del filtered_data is_data_in_cloud = False db_data.storage = models.StorageChoice.LOCAL + slogger.glob.info(f"DB Data Storage: {db_data.storage}") else: manifest = ImageManifestManager(db_data.get_manifest_path()) if job_file_mapping is not None and task_mode != 'annotation': raise ValidationError("job_file_mapping can't be used with sequence-based data like videos") + slogger.glob.info(f"Data: {data}") if data['server_files']: if db_data.storage == models.StorageChoice.LOCAL and not db_data.cloud_storage: # this means that the data has not been downloaded from the storage to the host + slogger.glob.info(f"Copying data from share point") _copy_data_from_share_point( (data['server_files'] + [manifest_file]) if manifest_file else data['server_files'], upload_dir, data.get('server_files_path'), data.get('server_files_exclude')) manifest_root = upload_dir + slogger.glob.info(f"Manifest Root: {manifest_root}") elif is_data_in_cloud: # we should sort media before sorting in the extractor because the manifest structure should match to the sorted media if job_file_mapping is not None: + slogger.glob.info(f"Job file mapping") + filtered_files = [] + for f in itertools.chain.from_iterable(job_file_mapping): + if f not in data['server_files']: + raise ValidationError(f"Job mapping file {f} is not specified in input files") + filtered_files.append(f) + data['server_files'] = filtered_files sorted_media = list(itertools.chain.from_iterable(job_file_mapping)) else: + slogger.glob.info(f"Sorting media") sorted_media = sort(media['image'], data['sorting_method']) media['image'] = sorted_media + + # Add logic to handle audio files from cloud storage + if db_data.storage == models.StorageChoice.CLOUD_STORAGE: + slogger.glob.info(f"Downloading data from cloud storage: {data['server_files']}") + _download_data_from_cloud_storage(db_data.cloud_storage, data['server_files'], upload_dir) + is_media_sorted = True if manifest_file: # Define task manifest content based on cloud storage manifest content and uploaded files + slogger.glob.info(f"Creating task manifest based on cloud storage manifest content and uploaded files") _create_task_manifest_based_on_cloud_storage_manifest( sorted_media, cloud_storage_manifest_prefix, cloud_storage_manifest, manifest) else: # without manifest file but with use_cache option # Define task manifest content based on list with uploaded files + slogger.glob.info(f"Creating task manifest from cloud data: {db_data.cloud_storage, sorted_media, manifest}") _create_task_manifest_from_cloud_data(db_data.cloud_storage, sorted_media, manifest) av_scan_paths(upload_dir) @@ -770,6 +802,7 @@ def _update_status(msg: str) -> None: # If upload from server_files image and directories # need to update images list by all found images in directories if (data['server_files']) and len(media['directory']) and len(media['image']): + slogger.glob.info(f"Updating images list by all found images in directories: {media['directory']}") media['image'].extend( [os.path.relpath(image, upload_dir) for image in MEDIA_TYPES['directory']['extractor']( @@ -1264,3 +1297,24 @@ def process_results(img_meta: list[tuple[str, int, tuple[int, int]]]): slogger.glob.info("Found frames {} for Data #{}".format(db_data.size, db_data.id)) _save_task_to_db(db_task, job_file_mapping=job_file_mapping) + + if MEDIA_TYPE == "video": + # Video Chunks overwrites + slogger.glob.info(f"Creating video chunks") + job_id_string = job.id + match = re.search(r'task-(\d+)', job_id_string) + + if match: + task_id = match.group(1) # Extracted '106' + response = MakeVideoChunks.make(task_id) + slogger.glob.info(response) + else: + response = { + "success" : False, + "message" : "No match found." + } + slogger.glob.error(response) + + # f = open( '/home/vignesh/Desktop/Desktop/IIITD/BTP.02/cvat/cvat/apps/engine/chunks.txt', 'w' ) + # f.write( 'dict = ' + repr(response) + '\n' ) + # f.close() \ No newline at end of file diff --git a/cvat/apps/iam/permissions.py b/cvat/apps/iam/permissions.py index b4e802378f96..62c1e22e518f 100644 --- a/cvat/apps/iam/permissions.py +++ b/cvat/apps/iam/permissions.py @@ -149,17 +149,17 @@ def get_resource(self): def check_access(self) -> PermissionResult: with make_requests_session() as session: response = session.post(self.url, json=self.payload) - output = response.json()['result'] + # output = response.json()['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) From 5975ac837db9d1662b6b4ef37366c88cac2d6990 Mon Sep 17 00:00:00 2001 From: Vignesh Goswami <88871046+Vignesh16879@users.noreply.github.com> Date: Mon, 11 Nov 2024 06:55:33 +0530 Subject: [PATCH 16/17] Update permissions.py to original --- cvat/apps/iam/permissions.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cvat/apps/iam/permissions.py b/cvat/apps/iam/permissions.py index 62c1e22e518f..f97d73210141 100644 --- a/cvat/apps/iam/permissions.py +++ b/cvat/apps/iam/permissions.py @@ -149,17 +149,17 @@ def get_resource(self): def check_access(self) -> PermissionResult: with make_requests_session() as session: response = session.post(self.url, json=self.payload) - # output = response.json()['result'] + output = response.json()['result'] 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) From edd588ffa3685d2ea2ab73fa259dff80b5b3d6a2 Mon Sep 17 00:00:00 2001 From: Vignesh Goswami <88871046+Vignesh16879@users.noreply.github.com> Date: Mon, 11 Nov 2024 06:57:08 +0530 Subject: [PATCH 17/17] added cloud storage for task creation and video chunk creator --- cvat/apps/iam/permissions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/apps/iam/permissions.py b/cvat/apps/iam/permissions.py index f97d73210141..b4e802378f96 100644 --- a/cvat/apps/iam/permissions.py +++ b/cvat/apps/iam/permissions.py @@ -151,7 +151,7 @@ def check_access(self) -> PermissionResult: response = session.post(self.url, json=self.payload) output = response.json()['result'] - allow = True + allow = False reasons = [] if isinstance(output, dict): allow = output['allow']