Skip to content

Commit

Permalink
feat(estela-api) Add migrations and save function for notifications
Browse files Browse the repository at this point in the history
feat(estela-api) Add notification support for user actions
  • Loading branch information
Jgaldos committed Feb 20, 2023
1 parent 9f94f47 commit 4fbe08b
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 7 deletions.
34 changes: 34 additions & 0 deletions estela-api/api/mixins.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from django.conf import settings
from django.core.mail import EmailMessage
from rest_framework import viewsets
from rest_framework.authentication import TokenAuthentication
from rest_framework.pagination import PageNumberPagination
from rest_framework.permissions import IsAuthenticated

from api.permissions import IsProjectUser, IsAdminOrReadOnly
from core.models import Notification


class APIPageNumberPagination(PageNumberPagination):
Expand All @@ -19,3 +21,35 @@ class BaseViewSet(viewsets.GenericViewSet):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated, IsProjectUser, IsAdminOrReadOnly]
pagination_class = APIPageNumberPagination


class NotificationsHandler(viewsets.GenericViewSet):

def get_users_email(self, project):
email_list = []
for user in project.users.all():
email_list.append(user.email)

return email_list

def save_notfication(self, user, message, project):
email_list = self.get_users_email(project)
email_body = f"""
Dear User
{message}
Estela Notification Service.
"""
notification = Notification(
message=message,
user=user,
notify_to=", ".join(email_list),
)
notification.save()
email = EmailMessage(
subject=f"NO REPLY: Notification alert on {project.name} project.",
body=email_body,
from_email=settings.VERIFICATION_EMAIL,
to=email_list,
)
email.send()
43 changes: 41 additions & 2 deletions estela-api/api/views/cronjob.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,19 @@
from rest_framework.response import Response

from api.filters import SpiderCronJobFilter
from api.mixins import BaseViewSet
from api.mixins import BaseViewSet, NotificationsHandler
from api.serializers.cronjob import (
SpiderCronJobCreateSerializer,
SpiderCronJobSerializer,
SpiderCronJobUpdateSerializer,
)
from core.cronjob import create_cronjob, disable_cronjob, run_cronjob_once
from core.models import Spider, SpiderCronJob
from core.models import Project, Spider, SpiderCronJob


class SpiderCronJobViewSet(
BaseViewSet,
NotificationsHandler,
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
Expand Down Expand Up @@ -93,6 +94,15 @@ def create(self, request, *args, **kwargs):
cronjob.schedule,
data_expiry_days=data_expiry_days,
)

# Send action notification
project = get_object_or_404(Project, pid=self.kwargs["pid"])
self.save_notfication(
user=request.user,
message=f"{request.user} has scheduled a new job at {spider.name} spider",
project=project,
)

headers = self.get_success_headers(serializer.data)
return Response(
serializer.data, status=status.HTTP_201_CREATED, headers=headers
Expand All @@ -109,6 +119,35 @@ def update(self, request, *args, **kwargs):
instance, data=request.data, partial=partial
)
serializer.is_valid(raise_exception=True)

status_changed = (
True
if "status" in request.data or instance.status == request.data["status"]
else False
)
schedule_changed = (
True
if "schedule" in request.data or instance.schedule != request.data["schedule"]
else False
)

# Send action notification
project = get_object_or_404(Project, pid=self.kwargs["pid"])
if status_changed:
message = f"{request.user.get_username()} has updated the status of a cronjob at {instance.spider.name} spider: {request.data['status']}"
self.save_notfication(
user=request.user,
message=message,
project=project,
)
if schedule_changed:
message = f"{request.user.get_username()} has updated the schedule of a cronjob at {instance.spider.name} spider: {request.data['schedule']}"
self.save_notfication(
user=request.user,
message=message,
project=project,
)

self.perform_update(serializer)

if getattr(instance, "_prefetched_objects_cache", None):
Expand Down
18 changes: 17 additions & 1 deletion estela-api/api/views/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from rest_framework.response import Response
from rest_framework.exceptions import ParseError, APIException, PermissionDenied

from api.mixins import BaseViewSet
from api.mixins import BaseViewSet, NotificationsHandler
from api.serializers.deploy import (
DeploySerializer,
DeployCreateSerializer,
Expand All @@ -19,6 +19,7 @@

class DeployViewSet(
BaseViewSet,
NotificationsHandler,
viewsets.ModelViewSet,
):
model_class = Deploy
Expand Down Expand Up @@ -66,6 +67,13 @@ def create(self, request, *args, **kwargs):
self.kwargs["pid"], serializer.data["did"], project.container_image
)

# Send action notification
self.save_notfication(
user=user,
message=f"{request.user} has made a new deployment for {project.name} Project.",
project=project,
)

headers = self.get_success_headers(serializer.data)
return Response(
serializer.data, status=status.HTTP_201_CREATED, headers=headers
Expand All @@ -91,5 +99,13 @@ def update(self, request, *args, **kwargs):
if getattr(instance, "_prefetched_objects_cache", None):
instance._prefetched_objects_cache = {}

# Send action notification
project = get_object_or_404(Project, pid=self.kwargs["pid"])
self.save_notfication(
user=request.user,
message=f"{request.user} has made an update for {project.name} Project.",
project=project,
)

headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_200_OK, headers=headers)
13 changes: 11 additions & 2 deletions estela-api/api/views/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,19 @@
from rest_framework.exceptions import ParseError

from api.filters import SpiderJobFilter
from api.mixins import BaseViewSet
from api.mixins import BaseViewSet, NotificationsHandler
from api.serializers.job import (
SpiderJobSerializer,
SpiderJobCreateSerializer,
SpiderJobUpdateSerializer,
)
from config.job_manager import job_manager
from core.models import Spider, SpiderJob
from core.models import Project, Spider, SpiderJob


class SpiderJobViewSet(
BaseViewSet,
NotificationsHandler,
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
Expand Down Expand Up @@ -138,6 +139,14 @@ def create(self, request, *args, **kwargs):
data_expiry_days=data_expiry_days,
)

# Send action notification
project = get_object_or_404(Project, pid=self.kwargs["pid"])
self.save_notfication(
user=request.user,
message=f"{request.user} has scheduled a new scheduled job at {spider.name} spider.",
project=project,
)

headers = self.get_success_headers(serializer.data)
return Response(
serializer.data, status=status.HTTP_201_CREATED, headers=headers
Expand Down
26 changes: 24 additions & 2 deletions estela-api/api/views/project.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from datetime import datetime, timedelta
from django.shortcuts import get_object_or_404

from api import errors
from api.mixins import BaseViewSet
from api.mixins import BaseViewSet, NotificationsHandler
from api.serializers.job import ProjectJobSerializer, SpiderJobSerializer
from api.serializers.cronjob import ProjectCronJobSerializer, SpiderCronJobSerializer
from api.serializers.project import (
Expand All @@ -28,7 +29,7 @@
from rest_framework.exceptions import NotFound, ParseError


class ProjectViewSet(BaseViewSet, viewsets.ModelViewSet):
class ProjectViewSet(BaseViewSet, NotificationsHandler, viewsets.ModelViewSet):
model_class = Project
queryset = Project.objects.all()
serializer_class = ProjectSerializer
Expand Down Expand Up @@ -82,9 +83,17 @@ def update(self, request, *args, **kwargs):
user_permision = serializer.validated_data.pop("user", "")
action = serializer.validated_data.pop("action", "")
permission = serializer.validated_data.pop("permission", "")
notification_message = None

if name:
instance.name = name
# Send action notification
project = get_object_or_404(Project, pid=self.kwargs["pid"])
self.save_notfication(
user=request.user,
message=f"{request.user} created a new Project: {instance.name}.",
project=project,
)
if user_email and user_email != user_permision:
user = User.objects.filter(email=user_email)
user_instance = User.objects.filter(email=user_permision)
Expand All @@ -99,24 +108,37 @@ def update(self, request, *args, **kwargs):
instance.users.add(
user, through_defaults={"permission": permission}
)
notification_message = f"{request.user} added {user.get_username()} as a new member of {instance.name} project, as {permission}."
elif action == "remove" and (
user.permission_set.get(project=instance).permission
!= Permission.OWNER_PERMISSION
):
instance.users.remove(user)
notification_message = f"{request.user} removed {user.get_username()} from {instance.name} project."
elif action == "update":
instance.users.remove(user)
instance.users.add(
user, through_defaults={"permission": permission}
)
notification_message = f"{request.user} changed {user.get_username()}'s role of {instance.name} project, to {permission}.",
else:
raise ParseError({"error": "Action not supported."})
else:

raise ParseError({"error": "Action not supported."})
else:
raise NotFound({"email": "User does not exist."})
serializer.save()

if notification_message:
# Send action notification
project = get_object_or_404(Project, pid=self.kwargs["pid"])
self.save_notfication(
user=request.user,
message=notification_message,
project=project,
)

headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_200_OK, headers=headers)

Expand Down
30 changes: 30 additions & 0 deletions estela-api/core/migrations/0024_notification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Generated by Django 3.1.14 on 2023-02-20 02:22

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),
('core', '0023_auto_20230113_0354'),
]

operations = [
migrations.CreateModel(
name='Notification',
fields=[
('nid', models.AutoField(help_text='A unique integer value identifying each notification', primary_key=True, serialize=False)),
('message', models.CharField(help_text='Notifications message.', max_length=1000)),
('notify_to', models.CharField(help_text='The direction where the notification will redirect.', max_length=100)),
('seen', models.BooleanField(default=False, help_text='Whether the notification was seen.')),
('created_at', models.DateTimeField(auto_now_add=True, help_text='Notification send date.')),
('user', models.ForeignKey(help_text='User whose this notification belongs to.', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-created_at'],
},
),
]
25 changes: 25 additions & 0 deletions estela-api/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,3 +400,28 @@ class UsageRecord(models.Model):

class Meta:
ordering = ["-created_at"]


class Notification(models.Model):
nid = models.AutoField(
primary_key=True,
help_text="A unique integer value identifying each notification",
)
message = models.CharField(max_length=1000, help_text="Notifications message.")
notify_to = models.CharField(
max_length=100, help_text="The direction where the notification will redirect."
)
seen = models.BooleanField(
default=False, help_text="Whether the notification was seen."
)
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
help_text="User whose this notification belongs to.",
)
created_at = models.DateTimeField(
auto_now_add=True, editable=False, help_text="Notification send date."
)

class Meta:
ordering = ["-created_at"]

0 comments on commit 4fbe08b

Please sign in to comment.