Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BITMAKER 2105: add notification support for user actions #142

Merged
merged 18 commits into from
Apr 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 9 additions & 0 deletions estela-api/api/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,12 @@
PAGE_NOT_FOUND = "Page not found."
INSUFFICIENT_PERMISSIONS = "You do not have the {} permissions to perform this action."
USER_NOT_FOUND = "User not found."
UNABLE_CHANGE_PASSWORD = "Unable to change password."
ERROR_SENDING_VERIFICATION_EMAIL = (
"Your user was created but there was an error sending"
" the verification email. Please try to log in later."
)
UNAUTHORIZED_PROFILE = "You are unauthorized to see this user profile."
SEND_EMAIL_LATER = (
"There was an error sending the verification email. Please try again later."
)
11 changes: 11 additions & 0 deletions estela-api/api/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from rest_framework.permissions import IsAuthenticated

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


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


class NotificationsHandlerMixin:
def save_notification(self, user, message, project):
notification = Notification(
message=message,
user=user,
project=project,
)
notification.save()
28 changes: 20 additions & 8 deletions estela-api/api/serializers/cronjob.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from rest_framework import serializers

from api import errors

from api.mixins import NotificationsHandlerMixin
from api.serializers.job_specific import (
SpiderJobArgSerializer,
SpiderJobEnvVarSerializer,
Expand Down Expand Up @@ -110,7 +112,9 @@ def create(self, validated_data):
return cronjob


class SpiderCronJobUpdateSerializer(serializers.ModelSerializer):
class SpiderCronJobUpdateSerializer(
serializers.ModelSerializer, NotificationsHandlerMixin
):
def validate(self, attrs):
attrs = super(SpiderCronJobUpdateSerializer, self).validate(attrs)
if "schedule" in attrs and not croniter.is_valid(attrs.get("schedule")):
Expand All @@ -131,37 +135,45 @@ class Meta:
)

def update(self, instance, validated_data):
user = instance["user"]
instance = instance["object"]
status = validated_data.get("status", "")
schedule = validated_data.get("schedule", "")
unique_collection = validated_data.get("unique_collection", False)
data_status = validated_data.get("data_status", "")
data_expiry_days = int(validated_data.get("data_expiry_days", 1))
name = instance.name
message = ""
if "schedule" in validated_data:
instance.schedule = schedule
update_schedule(name, schedule)
message = f"changed the schedule of Scheduled-job-{instance.cjid}."
if "status" in validated_data:
instance.status = status
if status == SpiderCronJob.ACTIVE_STATUS:
enable_cronjob(name)
message = f"enabled Scheduled-job-{instance.cjid}."
elif status == SpiderCronJob.DISABLED_STATUS:
disable_cronjob(name)
message = f"disabled Scheduled-job-{instance.cjid}."
if "unique_collection" in validated_data:
instance.unique_collection = unique_collection
if "data_status" in validated_data:
if data_status == DataStatus.PERSISTENT_STATUS:
instance.data_status = DataStatus.PERSISTENT_STATUS
elif data_status == DataStatus.PENDING_STATUS:
message = f"changed data persistence of Scheduled-job-{instance.cjid} to persistent."
elif data_status == DataStatus.PENDING_STATUS and data_expiry_days > 0:
instance.data_status = DataStatus.PENDING_STATUS
if data_expiry_days < 1:
raise serializers.ValidationError(
{"error": errors.POSITIVE_SMALL_INTEGER_FIELD}
)
else:
instance.data_expiry_days = data_expiry_days
instance.data_expiry_days = data_expiry_days
message = f"changed data persistence of Scheduled-job-{instance.cjid} to {data_expiry_days} days."
else:
raise serializers.ValidationError({"error": errors.INVALID_DATA_STATUS})

self.save_notification(
user=user,
message=message,
project=instance.spider.project,
)
instance.save()
return instance

Expand Down
24 changes: 24 additions & 0 deletions estela-api/api/serializers/notification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from rest_framework import serializers

from api.serializers.project import ProjectDetailSerializer, UserDetailSerializer
from core.models import Notification


class NotificationSerializer(serializers.ModelSerializer):
user = UserDetailSerializer(
required=True, help_text="User who performed the action."
)
project = ProjectDetailSerializer(
required=True, help_text="Project where the action was performed."
)

class Meta:
model = Notification
fields = (
"nid",
"user",
"message",
"project",
"seen",
"created_at",
)
17 changes: 17 additions & 0 deletions estela-api/api/serializers/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,20 @@ class Meta:
fields = ["user", "permission"]


class ProjectDetailSerializer(serializers.ModelSerializer):
pid = serializers.UUIDField(
read_only=True, help_text="A UUID identifying this project."
)
name = serializers.CharField(read_only=True, help_text="Project name.")

class Meta:
model = Project
fields = (
"pid",
"name",
)


class ProjectSerializer(serializers.ModelSerializer):
pid = serializers.UUIDField(
read_only=True, help_text="A UUID identifying this project."
Expand Down Expand Up @@ -109,6 +123,9 @@ class ProjectUpdateSerializer(serializers.ModelSerializer):
email = serializers.EmailField(
write_only=True, required=False, help_text="Email address."
)
name = serializers.CharField(
write_only=True, required=False, help_text="Project name."
)
action = serializers.ChoiceField(
write_only=True,
choices=ACTION_CHOICES,
Expand Down
6 changes: 6 additions & 0 deletions estela-api/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
auth as auth_views,
cronjob as cronjob_views,
job_data as job_data_views,
notification as notification_views,
stats as stats_views,
)

Expand All @@ -17,6 +18,11 @@
viewset=project_views.ProjectViewSet,
basename="project",
)
router.register(
prefix=r"notifications",
viewset=notification_views.NotificationViewSet,
basename="notification",
)
router.register(
prefix=r"projects/(?P<pid>[0-9a-z-]+)/deploys",
viewset=deploy_views.DeployViewSet,
Expand Down
17 changes: 4 additions & 13 deletions estela-api/api/views/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from rest_framework.exceptions import MethodNotAllowed, PermissionDenied
from rest_framework.response import Response

from api import errors
from api.exceptions import EmailServiceError, UserNotFoundError
from api.permissions import IsProfileUser
from api.serializers.auth import (
Expand Down Expand Up @@ -85,11 +86,7 @@ def register(self, request, *args, **kwargs):
try:
send_verification_email(user, request)
except Exception:
raise EmailServiceError(
{
"error": "Your user was created but there was an error sending the verification email. Please try to log in later."
}
)
raise EmailServiceError({"error": errors.ERROR_SENDING_VERIFICATION_EMAIL})
token, _ = Token.objects.get_or_create(user=user)
return Response(TokenSerializer(token).data)

Expand Down Expand Up @@ -161,9 +158,7 @@ def retrieve(self, request, *args, **kwargs):
)
if user != requested_user:
return Response(
data={
"error": "Unauthorized to see this profile, you are allowed to see only your profile."
},
data={"error": errors.UNAUTHORIZED_PROFILE},
status=status.HTTP_401_UNAUTHORIZED,
)

Expand Down Expand Up @@ -263,11 +258,7 @@ def request(self, request, *args, **kwargs):
try:
send_change_password_email(user)
except Exception:
raise EmailServiceError(
{
"error": "There was an error sending the password reset email. Please try again later."
}
)
raise EmailServiceError({"error": errors.SEND_EMAIL_LATER})
token, _ = Token.objects.get_or_create(user=user)
return Response(TokenSerializer(token).data)

Expand Down
15 changes: 13 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, NotificationsHandlerMixin
from api.serializers.cronjob import (
SpiderCronJobCreateSerializer,
SpiderCronJobSerializer,
SpiderCronJobUpdateSerializer,
)
from core.cronjob import create_cronjob, disable_cronjob, run_cronjob_once
from core.models import DataStatus, Spider, SpiderCronJob
from core.models import DataStatus, Spider, SpiderCronJob, Project


class SpiderCronJobViewSet(
BaseViewSet,
NotificationsHandlerMixin,
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 notification action
project = get_object_or_404(Project, pid=self.kwargs["pid"])
self.save_notification(
user=request.user,
message=f"scheduled a new Scheduled-job-{cronjob.cjid} for spider {spider.name}.",
project=project,
)

headers = self.get_success_headers(serializer.data)
return Response(
serializer.data, status=status.HTTP_201_CREATED, headers=headers
Expand All @@ -105,6 +115,7 @@ def create(self, request, *args, **kwargs):
def update(self, request, *args, **kwargs):
partial = kwargs.pop("partial", False)
instance = self.get_object()
instance = {"object": self.get_object(), "user": request.user}
serializer = SpiderCronJobUpdateSerializer(
instance, data=request.data, partial=partial
)
Expand Down
11 changes: 10 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, NotificationsHandlerMixin
from api.serializers.deploy import (
DeploySerializer,
DeployCreateSerializer,
Expand All @@ -19,6 +19,7 @@

class DeployViewSet(
BaseViewSet,
NotificationsHandlerMixin,
viewsets.ModelViewSet,
):
model_class = Deploy
Expand Down Expand Up @@ -91,5 +92,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_notification(
user=instance.user,
message=f"made a new Deploy #{serializer.data['did']}.",
project=project,
)

headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_200_OK, headers=headers)
14 changes: 11 additions & 3 deletions estela-api/api/views/job.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from datetime import date, timedelta
from django.shortcuts import get_object_or_404
from django_filters.rest_framework import DjangoFilterBackend
from drf_yasg.utils import swagger_auto_schema
Expand All @@ -8,18 +7,19 @@
from rest_framework.exceptions import ParseError

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


class SpiderJobViewSet(
BaseViewSet,
NotificationsHandlerMixin,
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
Expand Down Expand Up @@ -138,6 +138,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_notification(
user=request.user,
message=f"run a new job for spider {spider.name}.",
project=project,
)

headers = self.get_success_headers(serializer.data)
return Response(
serializer.data, status=status.HTTP_201_CREATED, headers=headers
Expand Down
20 changes: 20 additions & 0 deletions estela-api/api/views/notification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from rest_framework import viewsets

from api.mixins import BaseViewSet
from api.serializers.notification import NotificationSerializer
from core.models import Notification, Project


class NotificationViewSet(BaseViewSet, viewsets.ReadOnlyModelViewSet):
model_class = Notification
serializer_class = NotificationSerializer
lookup_field = "nid"
queryset = Notification.objects.all()

def get_queryset(self):
if self.request is None:
return Notification.objects.none()
projects = Project.objects.filter(
users__in=[self.request.user.id], deleted=False
)
return super().get_queryset().filter(project__in=projects)