This commit is contained in:
2026-05-11 03:27:21 +03:30
parent cf7cbb937c
commit d0e68a1a56
854 changed files with 102985 additions and 76 deletions
@@ -0,0 +1,80 @@
# Notification API Changes
## Added paginated notification list API
A new endpoint was added to return all notifications for a farm using pagination.
### Endpoint
`GET /api/notifications/list/`
### Query params
- `farm_uuid` (required): UUID of the farm
- `page` (optional): page number, default depends on DRF pagination behavior
- `page_size` (optional): number of items per page, default `10`, max `100`
### Behavior
- Requires authenticated user
- Returns notifications only if the farm belongs to the authenticated user
- Orders notifications by newest first using `created_at DESC, id DESC`
- Returns paginated response
- Returns `404` if the farm is not found or does not belong to the user
### Response shape
```json
{
"count": 12,
"next": "http://localhost:8000/api/notifications/list/?farm_uuid=<uuid>&page=2&page_size=5",
"previous": null,
"results": {
"code": 200,
"msg": "success",
"data": [
{
"uuid": "...",
"farm_uuid": "...",
"since_id": 12,
"title": "Alert",
"message": "Check sensor",
"level": "info",
"is_read": false,
"metadata": {},
"created_at": "2025-01-01T10:00:00Z"
}
]
}
}
```
## Long-poll behavior update
The `long-poll` notification logic was updated to prioritize unread notifications.
### Updated behavior
- Returns unread notifications first
- If unread notifications are fewer than `5`, fills the remaining slots with read notifications
- If unread notifications are `5` or more, returns only the first `5` unread notifications
### Notes
This behavior was implemented in the notification service layer so the existing long-poll endpoint automatically uses it.
## Files changed
- `notifications/views.py`
- `notifications/urls.py`
- `notifications/services.py`
- `notifications/tests.py`
## Tests added
Added tests for:
- paginated notification list for owned farm
- `404` for unowned farm on list API
- unread-first ordering in long-poll
- long-poll fallback with read notifications when unread count is below `5`
+7
View File
@@ -0,0 +1,7 @@
from django.apps import AppConfig
class NotificationsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "notifications"
verbose_name = "Notifications"
@@ -0,0 +1,41 @@
# Generated by Django 5.1.7 on 2025-02-20 00:00
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("farm_hub", "0006_seed_expanded_product_catalog"),
]
operations = [
migrations.CreateModel(
name="FarmNotification",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("uuid", models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, unique=True)),
("title", models.CharField(max_length=255)),
("message", models.TextField()),
("level", models.CharField(default="info", max_length=32)),
("is_read", models.BooleanField(default=False)),
("metadata", models.JSONField(blank=True, default=dict)),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"farm",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="notifications",
to="farm_hub.farmhub",
),
),
],
options={
"db_table": "farm_notifications",
"ordering": ["-created_at", "-id"],
},
),
]
@@ -0,0 +1,44 @@
# Generated by Django 5.1.15 on 2026-04-28 23:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("notifications", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="farmnotification",
name="endpoint",
field=models.CharField(blank=True, default="", max_length=64),
),
migrations.AddField(
model_name="farmnotification",
name="payload",
field=models.JSONField(blank=True, default=dict),
),
migrations.AddField(
model_name="farmnotification",
name="source_alert_id",
field=models.CharField(blank=True, db_index=True, default="", max_length=255),
),
migrations.AddField(
model_name="farmnotification",
name="source_metric_type",
field=models.CharField(blank=True, default="", max_length=255),
),
migrations.AddField(
model_name="farmnotification",
name="suggested_action",
field=models.TextField(blank=True, default=""),
),
migrations.AddField(
model_name="farmnotification",
name="updated_at",
field=models.DateTimeField(auto_now=True, default=None),
preserve_default=False,
),
]
@@ -0,0 +1 @@
+31
View File
@@ -0,0 +1,31 @@
import uuid as uuid_lib
from django.db import models
class FarmNotification(models.Model):
uuid = models.UUIDField(default=uuid_lib.uuid4, unique=True, editable=False, db_index=True)
farm = models.ForeignKey(
"farm_hub.FarmHub",
on_delete=models.CASCADE,
related_name="notifications",
)
title = models.CharField(max_length=255)
message = models.TextField()
level = models.CharField(max_length=32, default="info")
endpoint = models.CharField(max_length=64, blank=True, default="")
suggested_action = models.TextField(blank=True, default="")
source_alert_id = models.CharField(max_length=255, blank=True, default="", db_index=True)
source_metric_type = models.CharField(max_length=255, blank=True, default="")
payload = models.JSONField(default=dict, blank=True)
is_read = models.BooleanField(default=False)
metadata = models.JSONField(default=dict, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = "farm_notifications"
ordering = ["-created_at", "-id"]
def __str__(self):
return f"{self.farm_id}:{self.title}"
@@ -0,0 +1,30 @@
from rest_framework import serializers
from .models import FarmNotification
class FarmNotificationSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(read_only=True)
farm_uuid = serializers.UUIDField(source="farm.farm_uuid", read_only=True)
since_id = serializers.IntegerField(source="id", read_only=True)
class Meta:
model = FarmNotification
fields = [
"id",
"uuid",
"farm_uuid",
"since_id",
"endpoint",
"title",
"message",
"level",
"suggested_action",
"source_alert_id",
"source_metric_type",
"payload",
"is_read",
"metadata",
"created_at",
"updated_at",
]
+112
View File
@@ -0,0 +1,112 @@
import time
from django.db import OperationalError, ProgrammingError
from django.db.models import Case, IntegerField, QuerySet, Value, When
from django.utils import timezone
from farm_hub.models import FarmHub
from .models import FarmNotification
DEFAULT_POLL_TIMEOUT_SECONDS = 15
DEFAULT_POLL_INTERVAL_SECONDS = 1
def create_notification_for_farm_uuid(
*,
farm_uuid,
title,
message,
level="info",
endpoint="",
suggested_action="",
source_alert_id="",
source_metric_type="",
payload=None,
metadata=None,
):
farm = FarmHub.objects.filter(farm_uuid=farm_uuid).first()
if farm is None:
raise ValueError("Farm not found.")
try:
return FarmNotification.objects.create(
farm=farm,
title=title,
message=message,
level=level,
endpoint=endpoint,
suggested_action=suggested_action,
source_alert_id=source_alert_id,
source_metric_type=source_metric_type,
payload=payload or {},
metadata=metadata or {},
)
except (ProgrammingError, OperationalError) as exc:
raise ValueError("Notifications table is not migrated.") from exc
def get_recent_notifications_for_farm(*, farm: FarmHub, since_days=3, limit=10) -> QuerySet[FarmNotification]:
try:
since = timezone.now() - timezone.timedelta(days=max(since_days, 0))
return FarmNotification.objects.filter(farm=farm, created_at__gte=since).order_by("-created_at", "-id")[:limit]
except (ProgrammingError, OperationalError) as exc:
raise ValueError("Notifications table is not migrated.") from exc
def get_notifications_for_farm(*, farm: FarmHub, since_id=None) -> QuerySet[FarmNotification]:
try:
queryset = FarmNotification.objects.filter(farm=farm)
if since_id is not None:
queryset = queryset.filter(id__gt=since_id)
return queryset.order_by("created_at", "id")
except (ProgrammingError, OperationalError) as exc:
raise ValueError("Notifications table is not migrated.") from exc
def get_prioritized_notifications_for_farm(*, farm: FarmHub, since_id=None, limit=5) -> QuerySet[FarmNotification]:
try:
unread_queryset = get_notifications_for_farm(farm=farm, since_id=since_id).filter(is_read=False)
unread_count = unread_queryset.count()
if unread_count >= limit:
return unread_queryset[:limit]
fallback_limit = max(limit - unread_count, 0)
if fallback_limit == 0:
return unread_queryset
queryset = get_notifications_for_farm(farm=farm, since_id=since_id).annotate(
priority=Case(
When(is_read=False, then=Value(0)),
default=Value(1),
output_field=IntegerField(),
)
)
return queryset.order_by("priority", "created_at", "id")[:limit]
except (ProgrammingError, OperationalError) as exc:
raise ValueError("Notifications table is not migrated.") from exc
def mark_notifications_as_read(*, farm: FarmHub, slice_id: int) -> int:
try:
return FarmNotification.objects.filter(
farm=farm,
id__lte=slice_id,
is_read=False,
).update(is_read=True)
except (ProgrammingError, OperationalError) as exc:
raise ValueError("Notifications table is not migrated.") from exc
def long_poll_notifications(*, farm: FarmHub, since_id=None, timeout_seconds=DEFAULT_POLL_TIMEOUT_SECONDS, interval_seconds=DEFAULT_POLL_INTERVAL_SECONDS, limit=5):
deadline = time.monotonic() + max(timeout_seconds, 0)
while True:
notifications = list(get_prioritized_notifications_for_farm(farm=farm, since_id=since_id, limit=limit))
if notifications:
return notifications
if time.monotonic() >= deadline:
return []
time.sleep(max(interval_seconds, 0))
+353
View File
@@ -0,0 +1,353 @@
from unittest.mock import patch
from django.conf import settings
from django.contrib.auth import get_user_model
from django.test import TestCase, override_settings
from rest_framework.test import APIRequestFactory, force_authenticate
from farm_hub.models import FarmHub, FarmType
from .models import FarmNotification
from .services import create_notification_for_farm_uuid, get_prioritized_notifications_for_farm, long_poll_notifications, mark_notifications_as_read
from .views import ExternalNotificationIngestView, NotificationListView, NotificationLongPollView, NotificationMarkReadView
class NotificationServiceTests(TestCase):
def setUp(self):
self.user = get_user_model().objects.create_user(
username="notif-service-user",
password="secret123",
email="notif-service@example.com",
phone_number="09120000011",
)
self.farm_type = FarmType.objects.create(name="گلخانه")
self.farm = FarmHub.objects.create(
owner=self.user,
farm_type=self.farm_type,
name="Farm A",
)
def test_create_notification_for_farm_uuid_creates_record(self):
notification = create_notification_for_farm_uuid(
farm_uuid=self.farm.farm_uuid,
title="Irrigation alert",
message="Soil moisture dropped",
level="warning",
metadata={"sensor": "soil-1"},
)
self.assertEqual(notification.farm, self.farm)
self.assertEqual(notification.level, "warning")
self.assertEqual(notification.metadata["sensor"], "soil-1")
def test_create_notification_for_farm_uuid_raises_for_unknown_farm(self):
with self.assertRaisesMessage(ValueError, "Farm not found."):
create_notification_for_farm_uuid(
farm_uuid="11111111-1111-1111-1111-111111111111",
title="x",
message="y",
)
def test_long_poll_notifications_returns_new_notifications(self):
FarmNotification.objects.create(farm=self.farm, title="A", message="B")
notifications = long_poll_notifications(farm=self.farm, timeout_seconds=0)
self.assertEqual(len(notifications), 1)
self.assertEqual(notifications[0].title, "A")
def test_mark_notifications_as_read_marks_until_slice_id(self):
first = FarmNotification.objects.create(farm=self.farm, title="A", message="B")
second = FarmNotification.objects.create(farm=self.farm, title="C", message="D")
third = FarmNotification.objects.create(farm=self.farm, title="E", message="F")
marked_count = mark_notifications_as_read(farm=self.farm, slice_id=second.id)
self.assertEqual(marked_count, 2)
first.refresh_from_db()
second.refresh_from_db()
third.refresh_from_db()
self.assertTrue(first.is_read)
self.assertTrue(second.is_read)
self.assertFalse(third.is_read)
def test_get_prioritized_notifications_for_farm_returns_unread_first_and_fills_with_read(self):
unread_one = FarmNotification.objects.create(farm=self.farm, title="Unread 1", message="A")
unread_two = FarmNotification.objects.create(farm=self.farm, title="Unread 2", message="B")
read_one = FarmNotification.objects.create(farm=self.farm, title="Read 1", message="C", is_read=True)
read_two = FarmNotification.objects.create(farm=self.farm, title="Read 2", message="D", is_read=True)
notifications = list(get_prioritized_notifications_for_farm(farm=self.farm, limit=5))
self.assertEqual([notification.id for notification in notifications], [unread_one.id, unread_two.id, read_one.id, read_two.id])
def test_long_poll_notifications_limits_to_five_unread_notifications(self):
for index in range(6):
FarmNotification.objects.create(farm=self.farm, title=f"Unread {index}", message="B")
notifications = long_poll_notifications(farm=self.farm, timeout_seconds=0, limit=5)
self.assertEqual(len(notifications), 5)
self.assertTrue(all(notification.is_read is False for notification in notifications))
class NotificationLongPollViewTests(TestCase):
def setUp(self):
self.factory = APIRequestFactory()
self.user = get_user_model().objects.create_user(
username="notif-view-user",
password="secret123",
email="notif-view@example.com",
phone_number="09120000012",
)
self.other_user = get_user_model().objects.create_user(
username="notif-other-user",
password="secret123",
email="notif-other@example.com",
phone_number="09120000013",
)
self.farm_type = FarmType.objects.create(name="دامداری")
self.farm = FarmHub.objects.create(
owner=self.user,
farm_type=self.farm_type,
name="Farm B",
)
def test_long_poll_view_returns_notifications_for_owned_farm(self):
notification = FarmNotification.objects.create(farm=self.farm, title="Alert", message="Check sensor")
request = self.factory.get(f"/api/notifications/long-poll/?farm_uuid={self.farm.farm_uuid}&timeout=0")
force_authenticate(request, user=self.user)
response = NotificationLongPollView.as_view()(request)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data["data"]), 1)
self.assertEqual(response.data["data"][0]["title"], "Alert")
self.assertEqual(response.data["data"][0]["since_id"], notification.id)
self.assertFalse(response.data["data"][0]["is_read"])
def test_long_poll_view_returns_unread_first_then_read_when_unread_is_less_than_five(self):
unread_one = FarmNotification.objects.create(farm=self.farm, title="Unread 1", message="A")
unread_two = FarmNotification.objects.create(farm=self.farm, title="Unread 2", message="B")
read_one = FarmNotification.objects.create(farm=self.farm, title="Read 1", message="C", is_read=True)
read_two = FarmNotification.objects.create(farm=self.farm, title="Read 2", message="D", is_read=True)
request = self.factory.get(f"/api/notifications/long-poll/?farm_uuid={self.farm.farm_uuid}&timeout=0")
force_authenticate(request, user=self.user)
response = NotificationLongPollView.as_view()(request)
self.assertEqual(response.status_code, 200)
self.assertEqual(
[item["since_id"] for item in response.data["data"]],
[unread_one.id, unread_two.id, read_one.id, read_two.id],
)
self.assertEqual([item["is_read"] for item in response.data["data"]], [False, False, True, True])
def test_long_poll_view_returns_404_for_unowned_farm(self):
request = self.factory.get(f"/api/notifications/long-poll/?farm_uuid={self.farm.farm_uuid}&timeout=0")
force_authenticate(request, user=self.other_user)
response = NotificationLongPollView.as_view()(request)
self.assertEqual(response.status_code, 404)
self.assertEqual(response.data["msg"], "Farm not found.")
@patch("notifications.views.long_poll_notifications")
def test_long_poll_view_passes_since_id(self, mocked_long_poll):
mocked_long_poll.return_value = []
request = self.factory.get(
f"/api/notifications/long-poll/?farm_uuid={self.farm.farm_uuid}&since_id=5&timeout=0"
)
force_authenticate(request, user=self.user)
response = NotificationLongPollView.as_view()(request)
self.assertEqual(response.status_code, 200)
mocked_long_poll.assert_called_once()
self.assertEqual(mocked_long_poll.call_args.kwargs["since_id"], 5)
class NotificationMarkReadViewTests(TestCase):
def setUp(self):
self.factory = APIRequestFactory()
self.user = get_user_model().objects.create_user(
username="notif-mark-user",
password="secret123",
email="notif-mark@example.com",
phone_number="09120000015",
)
self.other_user = get_user_model().objects.create_user(
username="notif-mark-other-user",
password="secret123",
email="notif-mark-other@example.com",
phone_number="09120000016",
)
self.farm_type = FarmType.objects.create(name="باغ")
self.farm = FarmHub.objects.create(
owner=self.user,
farm_type=self.farm_type,
name="Farm D",
)
def test_mark_read_view_marks_notifications_up_to_slice_id(self):
first = FarmNotification.objects.create(farm=self.farm, title="Alert 1", message="Check sensor")
second = FarmNotification.objects.create(farm=self.farm, title="Alert 2", message="Check pump")
third = FarmNotification.objects.create(farm=self.farm, title="Alert 3", message="Check valve")
request = self.factory.post(
"/api/notifications/mark-as-read/",
{
"farm_uuid": str(self.farm.farm_uuid),
"slice_id": second.id,
},
format="json",
)
force_authenticate(request, user=self.user)
response = NotificationMarkReadView.as_view()(request)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["marked_count"], 2)
first.refresh_from_db()
second.refresh_from_db()
third.refresh_from_db()
self.assertTrue(first.is_read)
self.assertTrue(second.is_read)
self.assertFalse(third.is_read)
def test_mark_read_view_returns_404_for_unowned_farm(self):
notification = FarmNotification.objects.create(farm=self.farm, title="Alert", message="Check sensor")
request = self.factory.post(
"/api/notifications/mark-as-read/",
{
"farm_uuid": str(self.farm.farm_uuid),
"slice_id": notification.id,
},
format="json",
)
force_authenticate(request, user=self.other_user)
response = NotificationMarkReadView.as_view()(request)
self.assertEqual(response.status_code, 404)
self.assertEqual(response.data["msg"], "Farm not found.")
class NotificationListViewTests(TestCase):
def setUp(self):
self.factory = APIRequestFactory()
self.user = get_user_model().objects.create_user(
username="notif-list-user",
password="secret123",
email="notif-list@example.com",
phone_number="09120000017",
)
self.other_user = get_user_model().objects.create_user(
username="notif-list-other-user",
password="secret123",
email="notif-list-other@example.com",
phone_number="09120000018",
)
self.farm_type = FarmType.objects.create(name="مرغداری")
self.farm = FarmHub.objects.create(
owner=self.user,
farm_type=self.farm_type,
name="Farm E",
)
def test_list_view_returns_paginated_notifications_for_owned_farm(self):
for index in range(12):
FarmNotification.objects.create(farm=self.farm, title=f"Alert {index}", message="Check sensor")
request = self.factory.get(
f"/api/notifications/list/?farm_uuid={self.farm.farm_uuid}&page=2&page_size=5"
)
force_authenticate(request, user=self.user)
response = NotificationListView.as_view()(request)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["count"], 12)
self.assertEqual(len(response.data["results"]["data"]), 5)
self.assertEqual(response.data["results"]["code"], 200)
def test_list_view_returns_404_for_unowned_farm(self):
request = self.factory.get(f"/api/notifications/list/?farm_uuid={self.farm.farm_uuid}")
force_authenticate(request, user=self.other_user)
response = NotificationListView.as_view()(request)
self.assertEqual(response.status_code, 404)
self.assertEqual(response.data["msg"], "Farm not found.")
@override_settings(EXTERNAL_NOTIFICATION_API_KEY="12345")
class ExternalNotificationIngestViewTests(TestCase):
def setUp(self):
self.factory = APIRequestFactory()
self.user = get_user_model().objects.create_user(
username="notif-external-user",
password="secret123",
email="notif-external@example.com",
phone_number="09120000014",
)
self.farm_type = FarmType.objects.create(name="آبی")
self.farm = FarmHub.objects.create(
owner=self.user,
farm_type=self.farm_type,
name="Farm C",
)
def test_external_ingest_requires_api_key(self):
request = self.factory.post(
"/api/notifications/external/ingest/",
{
"farm_uuid": str(self.farm.farm_uuid),
"title": "external",
"message": "payload",
},
format="json",
)
response = ExternalNotificationIngestView.as_view()(request)
self.assertEqual(response.status_code, 401)
def test_external_ingest_creates_notification_with_valid_api_key(self):
request = self.factory.post(
"/api/notifications/external/ingest/",
{
"farm_uuid": str(self.farm.farm_uuid),
"title": "Pump alert",
"message": "Pump disconnected",
"level": "critical",
"metadata": {"source": "external-service"},
},
format="json",
HTTP_X_API_KEY=settings.EXTERNAL_NOTIFICATION_API_KEY,
)
response = ExternalNotificationIngestView.as_view()(request)
self.assertEqual(response.status_code, 201)
self.assertEqual(response.data["data"]["title"], "Pump alert")
self.assertTrue(
FarmNotification.objects.filter(farm=self.farm, title="Pump alert", level="critical").exists()
)
def test_external_ingest_returns_404_for_unknown_farm(self):
request = self.factory.post(
"/api/notifications/external/ingest/",
{
"farm_uuid": "11111111-1111-1111-1111-111111111111",
"title": "Pump alert",
"message": "Pump disconnected",
},
format="json",
HTTP_X_API_KEY=settings.EXTERNAL_NOTIFICATION_API_KEY,
)
response = ExternalNotificationIngestView.as_view()(request)
self.assertEqual(response.status_code, 404)
self.assertEqual(response.data["msg"], "Farm not found.")
+9
View File
@@ -0,0 +1,9 @@
from django.urls import path
from .views import NotificationListView, NotificationLongPollView, NotificationMarkReadView
urlpatterns = [
path("list/", NotificationListView.as_view(), name="notification-list"),
path("long-poll/", NotificationLongPollView.as_view(), name="notification-long-poll"),
path("mark-as-read/", NotificationMarkReadView.as_view(), name="notification-mark-as-read"),
]
+160
View File
@@ -0,0 +1,160 @@
from django.conf import settings
from drf_spectacular.utils import OpenApiParameter, OpenApiTypes, extend_schema
from rest_framework import serializers, status
from rest_framework.pagination import PageNumberPagination
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from config.swagger import code_response
from farm_hub.models import FarmHub
from .serializers import FarmNotificationSerializer
from .services import long_poll_notifications, mark_notifications_as_read
class NotificationLongPollQuerySerializer(serializers.Serializer):
farm_uuid = serializers.UUIDField(default="11111111-1111-1111-1111-111111111111")
since_id = serializers.IntegerField(required=False, min_value=1)
timeout = serializers.IntegerField(required=False, min_value=0, max_value=60)
class NotificationListQuerySerializer(serializers.Serializer):
farm_uuid = serializers.UUIDField(default="11111111-1111-1111-1111-111111111111")
page = serializers.IntegerField(required=False, min_value=1)
page_size = serializers.IntegerField(required=False, min_value=1, max_value=100)
class NotificationPagination(PageNumberPagination):
page_size = 10
page_size_query_param = "page_size"
max_page_size = 100
class NotificationMarkReadSerializer(serializers.Serializer):
farm_uuid = serializers.UUIDField(default="11111111-1111-1111-1111-111111111111")
slice_id = serializers.IntegerField(min_value=1)
def get_owned_farm(*, farm_uuid, user):
return FarmHub.objects.filter(farm_uuid=farm_uuid, owner=user).first()
class NotificationLongPollView(APIView):
permission_classes = [IsAuthenticated]
@extend_schema(
tags=["Notifications"],
parameters=[NotificationLongPollQuerySerializer],
responses={
200: code_response("NotificationLongPollResponse", data=FarmNotificationSerializer(many=True)),
404: code_response("NotificationLongPollNotFoundResponse"),
503: code_response("NotificationLongPollNotificationsUnavailableResponse"),
},
)
def get(self, request):
serializer = NotificationLongPollQuerySerializer(data=request.query_params)
serializer.is_valid(raise_exception=True)
farm = get_owned_farm(
farm_uuid=serializer.validated_data["farm_uuid"],
user=request.user,
)
if farm is None:
return Response({"code": 404, "msg": "Farm not found."}, status=status.HTTP_404_NOT_FOUND)
try:
notifications = long_poll_notifications(
farm=farm,
since_id=serializer.validated_data.get("since_id"),
timeout_seconds=serializer.validated_data.get("timeout", 15),
)
except ValueError as exc:
if str(exc) == "Notifications table is not migrated.":
return Response(
{"code": 503, "msg": "Notifications table is not ready. Run migrations."},
status=status.HTTP_503_SERVICE_UNAVAILABLE,
)
raise
data = FarmNotificationSerializer(notifications, many=True).data
return Response({"code": 200, "msg": "success", "data": data}, status=status.HTTP_200_OK)
class NotificationListView(APIView):
permission_classes = [IsAuthenticated]
pagination_class = NotificationPagination
@extend_schema(
tags=["Notifications"],
parameters=[NotificationListQuerySerializer],
responses={
200: code_response("NotificationListResponse"),
404: code_response("NotificationListNotFoundResponse"),
},
)
def get(self, request):
serializer = NotificationListQuerySerializer(data=request.query_params)
serializer.is_valid(raise_exception=True)
farm = get_owned_farm(
farm_uuid=serializer.validated_data["farm_uuid"],
user=request.user,
)
if farm is None:
return Response({"code": 404, "msg": "Farm not found."}, status=status.HTTP_404_NOT_FOUND)
paginator = self.pagination_class()
notifications = farm.notifications.all().order_by("-created_at", "-id")
page = paginator.paginate_queryset(notifications, request, view=self)
data = FarmNotificationSerializer(page, many=True).data
return paginator.get_paginated_response({
"code": 200,
"msg": "success",
"data": data,
})
class NotificationMarkReadView(APIView):
permission_classes = [IsAuthenticated]
@extend_schema(
tags=["Notifications"],
request=NotificationMarkReadSerializer,
responses={
200: code_response(
"NotificationMarkReadResponse",
extra_fields={"marked_count": serializers.IntegerField()},
),
404: code_response("NotificationMarkReadNotFoundResponse"),
503: code_response("NotificationMarkReadUnavailableResponse"),
},
)
def post(self, request):
serializer = NotificationMarkReadSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
farm = get_owned_farm(
farm_uuid=serializer.validated_data["farm_uuid"],
user=request.user,
)
if farm is None:
return Response({"code": 404, "msg": "Farm not found."}, status=status.HTTP_404_NOT_FOUND)
try:
marked_count = mark_notifications_as_read(
farm=farm,
slice_id=serializer.validated_data["slice_id"],
)
except ValueError as exc:
if str(exc) == "Notifications table is not migrated.":
return Response(
{"code": 503, "msg": "Notifications table is not ready. Run migrations."},
status=status.HTTP_503_SERVICE_UNAVAILABLE,
)
raise
return Response(
{"code": 200, "msg": "success", "marked_count": marked_count},
status=status.HTTP_200_OK,
)