Skip to content

Commit

Permalink
Merge pull request #1394 from yogeshojha/1392-feat-builtin-notificati…
Browse files Browse the repository at this point in the history
…on-system

feat: Builtin notification system in reNgine #1392
  • Loading branch information
yogeshojha authored Aug 31, 2024
2 parents df02d56 + 7b96a21 commit 7b1e6e9
Show file tree
Hide file tree
Showing 21 changed files with 943 additions and 43 deletions.
26 changes: 24 additions & 2 deletions web/api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,35 @@
from dashboard.models import *
from django.contrib.humanize.templatetags.humanize import (naturalday,
naturaltime)
from django.contrib.humanize.templatetags.humanize import (naturalday, naturaltime)
from django.db.models import F, JSONField, Value
from recon_note.models import *
from reNgine.common_func import *
from rest_framework import serializers
from scanEngine.models import *
from startScan.models import *
from targetApp.models import *
from dashboard.models import InAppNotification


class InAppNotificationSerializer(serializers.ModelSerializer):
class Meta:
model = InAppNotification
fields = [
'id',
'title',
'description',
'icon',
'is_read',
'created_at',
'notification_type',
'status',
'redirect_link',
'open_in_new_tab',
'project'
]
read_only_fields = ['id', 'created_at']

def get_project_name(self, obj):
return obj.project.name if obj.project else None


class SearchHistorySerializer(serializers.ModelSerializer):
Expand Down
1 change: 1 addition & 0 deletions web/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
router.register(r'listIps', IpAddressViewSet)
router.register(r'listActivityLogs', ListActivityLogsViewSet)
router.register(r'listScanLogs', ListScanLogsViewSet)
router.register(r'notifications', InAppNotificationManagerViewSet, basename='notification')

urlpatterns = [
url('^', include(router.urls)),
Expand Down
85 changes: 82 additions & 3 deletions web/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.status import HTTP_400_BAD_REQUEST
from rest_framework.status import HTTP_400_BAD_REQUEST, HTTP_204_NO_CONTENT
from rest_framework.decorators import action

from recon_note.models import *
from reNgine.celery import app
Expand All @@ -33,6 +34,71 @@
logger = logging.getLogger(__name__)


class InAppNotificationManagerViewSet(viewsets.ModelViewSet):
"""
This class manages the notification model, provided CRUD operation on notif model
such as read notif, clear all, fetch all notifications etc
"""
serializer_class = InAppNotificationSerializer
pagination_class = None

def get_queryset(self):
# we will see later if user based notif is needed
# return InAppNotification.objects.filter(user=self.request.user)
project_slug = self.request.query_params.get('project_slug')
queryset = InAppNotification.objects.all()
if project_slug:
queryset = queryset.filter(
Q(project__slug=project_slug) | Q(notification_type='system')
)
return queryset.order_by('-created_at')

@action(detail=False, methods=['post'])
def mark_all_read(self, request):
# marks all notification read
project_slug = self.request.query_params.get('project_slug')
queryset = self.get_queryset()

if project_slug:
queryset = queryset.filter(
Q(project__slug=project_slug) | Q(notification_type='system')
)
queryset.update(is_read=True)
return Response(status=HTTP_204_NO_CONTENT)

@action(detail=True, methods=['post'])
def mark_read(self, request, pk=None):
# mark individual notification read when cliked
notification = self.get_object()
notification.is_read = True
notification.save()
return Response(status=HTTP_204_NO_CONTENT)

@action(detail=False, methods=['get'])
def unread_count(self, request):
# this fetches the count for unread notif mainly for the badge
project_slug = self.request.query_params.get('project_slug')
queryset = self.get_queryset()
if project_slug:
queryset = queryset.filter(
Q(project__slug=project_slug) | Q(notification_type='system')
)
count = queryset.filter(is_read=False).count()
return Response({'count': count})

@action(detail=False, methods=['post'])
def clear_all(self, request):
# when clicked on the clear button this must be called to clear all notif
project_slug = self.request.query_params.get('project_slug')
queryset = self.get_queryset()
if project_slug:
queryset = queryset.filter(
Q(project__slug=project_slug) | Q(notification_type='system')
)
queryset.delete()
return Response(status=HTTP_204_NO_CONTENT)


class OllamaManager(APIView):
def get(self, request):
"""
Expand Down Expand Up @@ -943,8 +1009,21 @@ def get(self, request):
return_response['status'] = True
return_response['latest_version'] = latest_version
return_response['current_version'] = current_version
return_response['update_available'] = version.parse(current_version) < version.parse(latest_version)
if version.parse(current_version) < version.parse(latest_version):
is_version_update_available = version.parse(current_version) < version.parse(latest_version)

# if is_version_update_available then we should create inapp notification
create_inappnotification(
title='reNgine Update Available',
description=f'Update to version {latest_version} is available',
notification_type=SYSTEM_LEVEL_NOTIFICATION,
project_slug=None,
icon='mdi-update',
redirect_link='https://github.com/yogeshojha/rengine/releases',
open_in_new_tab=True
)

return_response['update_available'] = is_version_update_available
if is_version_update_available:
return_response['changelog'] = response[0]['body']

return Response(return_response)
Expand Down
1 change: 1 addition & 0 deletions web/dashboard/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
admin.site.register(Project)
admin.site.register(OpenAiAPIKey)
admin.site.register(NetlasAPIKey)
admin.site.register(InAppNotification)
27 changes: 27 additions & 0 deletions web/dashboard/migrations/0002_notification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 3.2.23 on 2024-08-30 00:47

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('dashboard', '0001_initial'),
]

operations = [
migrations.CreateModel(
name='Notification',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255)),
('description', models.TextField()),
('icon', models.CharField(max_length=50)),
('is_read', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
options={
'ordering': ['-created_at'],
},
),
]
24 changes: 24 additions & 0 deletions web/dashboard/migrations/0003_auto_20240830_0135.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 3.2.23 on 2024-08-30 01:35

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('dashboard', '0002_notification'),
]

operations = [
migrations.AddField(
model_name='notification',
name='notification_type',
field=models.CharField(choices=[('system', 'System-wide'), ('project', 'Project-specific')], default='system', max_length=10),
),
migrations.AddField(
model_name='notification',
name='project',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dashboard.project'),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 3.2.23 on 2024-08-30 01:40

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('dashboard', '0003_auto_20240830_0135'),
]

operations = [
migrations.RenameModel(
old_name='Notification',
new_name='InAppNotification',
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.23 on 2024-08-31 01:21

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('dashboard', '0004_rename_notification_inappnotification'),
]

operations = [
migrations.AlterField(
model_name='inappnotification',
name='notification_type',
field=models.CharField(choices=[('system', 'system'), ('project', 'project')], default='system', max_length=10),
),
]
18 changes: 18 additions & 0 deletions web/dashboard/migrations/0006_inappnotification_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.23 on 2024-08-31 02:29

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('dashboard', '0005_alter_inappnotification_notification_type'),
]

operations = [
migrations.AddField(
model_name='inappnotification',
name='status',
field=models.CharField(choices=[('success', 'Success'), ('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], default='info', max_length=10),
),
]
23 changes: 23 additions & 0 deletions web/dashboard/migrations/0007_auto_20240831_0608.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 3.2.23 on 2024-08-31 06:08

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('dashboard', '0006_inappnotification_status'),
]

operations = [
migrations.AddField(
model_name='inappnotification',
name='open_in_new_tab',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='inappnotification',
name='redirect_link',
field=models.URLField(blank=True, max_length=255, null=True),
),
]
29 changes: 28 additions & 1 deletion web/dashboard/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from django.db import models

from reNgine.definitions import *

class SearchHistory(models.Model):
query = models.CharField(max_length=1000)
Expand Down Expand Up @@ -41,3 +41,30 @@ class NetlasAPIKey(models.Model):

def __str__(self):
return self.key


class InAppNotification(models.Model):
project = models.ForeignKey(Project, on_delete=models.CASCADE, null=True, blank=True)
notification_type = models.CharField(max_length=10, choices=NOTIFICATION_TYPES, default='system')
status = models.CharField(max_length=10, choices=NOTIFICATION_STATUS_TYPES, default='info')
title = models.CharField(max_length=255)
description = models.TextField()
icon = models.CharField(max_length=50) # mdi icon class name
is_read = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
redirect_link = models.URLField(max_length=255, blank=True, null=True)
open_in_new_tab = models.BooleanField(default=False)

class Meta:
ordering = ['-created_at']

def __str__(self):
if self.notification_type == 'system':
return f"System wide notif: {self.title}"
else:
return f"Project wide notif: {self.project.name}: {self.title}"

@property
def is_system_wide(self):
# property to determine if the notification is system wide or project specific
return self.notification_type == 'system'
64 changes: 64 additions & 0 deletions web/reNgine/common_func.py
Original file line number Diff line number Diff line change
Expand Up @@ -1549,3 +1549,67 @@ def save_domain_info_to_db(target, domain_info):
domain.save()

return domain_info_obj


def create_inappnotification(
title,
description,
notification_type=SYSTEM_LEVEL_NOTIFICATION,
project_slug=None,
icon="mdi-bell",
is_read=False,
status='info',
redirect_link=None,
open_in_new_tab=False
):
"""
This function will create an inapp notification
Inapp Notification not to be confused with Notification model
that is used for sending alerts on telegram, slack etc.
Inapp notification is used to show notification on the web app
Args:
title: str: Title of the notification
description: str: Description of the notification
notification_type: str: Type of the notification, it can be either
SYSTEM_LEVEL_NOTIFICATION or PROJECT_LEVEL_NOTIFICATION
project_slug: str: Slug of the project, if notification is PROJECT_LEVEL_NOTIFICATION
icon: str: Icon of the notification, only use mdi icons
is_read: bool: Whether the notification is read or not, default is False
status: str: Status of the notification (success, info, warning, error), default is info
redirect_link: str: Link to redirect when notification is clicked
open_in_new_tab: bool: Whether to open the redirect link in a new tab, default is False
Returns:
ValueError: if error
InAppNotification: InAppNotification object if successful
"""
logger.info('Creating InApp Notification with title: %s', title)
if notification_type not in [SYSTEM_LEVEL_NOTIFICATION, PROJECT_LEVEL_NOTIFICATION]:
raise ValueError("Invalid notification type")

if status not in [choice[0] for choice in NOTIFICATION_STATUS_TYPES]:
raise ValueError("Invalid notification status")

project = None
if notification_type == PROJECT_LEVEL_NOTIFICATION:
if not project_slug:
raise ValueError("Project slug is required for project level notification")
try:
project = Project.objects.get(slug=project_slug)
except Project.DoesNotExist as e:
raise ValueError(f"No project exists: {e}")

notification = InAppNotification(
title=title,
description=description,
notification_type=notification_type,
project=project,
icon=icon,
is_read=is_read,
status=status,
redirect_link=redirect_link,
open_in_new_tab=open_in_new_tab
)
notification.save()
return notification
Loading

0 comments on commit 7b1e6e9

Please sign in to comment.