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

Notifications #617

Merged
merged 25 commits into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
709c45a
add notifications models & admin class
larisa17 Jun 17, 2024
214b027
add generators & tests
larisa17 Jun 18, 2024
599dd15
WIP notifications
larisa17 Jun 20, 2024
1a6a81a
test
larisa17 Jun 20, 2024
f43d39f
fix: fixture in test_notification_fixture
nutrina Jun 20, 2024
5027656
wip - adjuns notification api , model & tests
larisa17 Jun 20, 2024
9f74712
fix get active notification , return generic notifications
larisa17 Jun 20, 2024
285b6a9
update api response, return is_read info
larisa17 Jun 20, 2024
13921b7
add deduplication notification & adjust notification generation logic
larisa17 Jun 20, 2024
b2e4f33
update ceramic cache migrations
larisa17 Jun 20, 2024
2c08ff7
update dismissal endpoint
larisa17 Jun 20, 2024
ae48503
add the expired stamp and on chain expiration notifications
larisa17 Jun 20, 2024
5b53b3b
update migrations
larisa17 Jun 20, 2024
73c3070
feat: backfill expiration and and issuance date in ceramic cache
tim-schultz Jun 20, 2024
e8b296a
fix: notification endpoint throwing 500s
tim-schultz Jun 20, 2024
028ffeb
fix: expiration notifications
tim-schultz Jun 21, 2024
ead3c47
fix: remove unnecesary setter
tim-schultz Jun 21, 2024
56319dd
fix: failing test
nutrina Jun 24, 2024
e83b889
fix: verification of returned items
nutrina Jun 24, 2024
9545a3d
add tests for duplication notifications
larisa17 Jun 25, 2024
51c8565
allow blank fields for notifications
larisa17 Jun 25, 2024
320634f
filter only the recent dedup events & limit tops 20 notifications
larisa17 Jun 28, 2024
9092288
add missing tests
larisa17 Jul 4, 2024
5c17e28
wip - adjuns notification api , model & tests
larisa17 Jun 20, 2024
871d750
fix migration error
larisa17 Jul 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions api/ceramic_cache/api/v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ def handle_add_stamps(
stamp=p.stamp,
updated_at=now,
compose_db_save_status=CeramicCache.ComposeDBSaveStatus.PENDING,
issuance_date=p.stamp.get("issuanceDate", None),
expiration_date=p.stamp.get("expirationDate", None),
)
for p in payload
]
Expand Down Expand Up @@ -236,6 +238,8 @@ def handle_patch_stamps(
stamp=p.stamp,
updated_at=now,
compose_db_save_status=CeramicCache.ComposeDBSaveStatus.PENDING,
issuance_date=p.stamp.get("issuanceDate", None),
expiration_date=p.stamp.get("expirationDate", None),
)
for p in payload
if p.stamp
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from django.core.management.base import BaseCommand
from django.db import connection
from ceramic_cache.models import CeramicCache


class Command(BaseCommand):
help = "Backfills expiration_date and issuance_date from JSON data"

def handle(self, *args, **options):
batch_size = 10000
max_id = CeramicCache.objects.latest("id").id
current_id = 0

while current_id < max_id:
with connection.cursor() as cursor:
cursor.execute(
"""
UPDATE ceramic_cache_ceramiccache
SET
expiration_date = (stamp::json->>'expirationDate')::timestamp,
issuance_date = (stamp::json->>'issuanceDate')::timestamp
WHERE
id > %s
AND id <= %s
AND (expiration_date IS NULL OR issuance_date IS NULL)
""",
[current_id, current_id + batch_size],
)

self.stdout.write(
self.style.SUCCESS(f"Backfilled up to id {current_id + batch_size}")
)

current_id += batch_size
self.stdout.write(f"Processed up to id {current_id}")

self.stdout.write(self.style.SUCCESS("Data backfill completed successfully"))
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 4.2.6 on 2024-06-20 21:31

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("ceramic_cache", "0020_alter_ceramiccache_compose_db_save_status_and_more"),
]

operations = [
migrations.AddField(
model_name="ceramiccache",
name="expiration_date",
field=models.DateTimeField(db_index=True, null=True),
),
migrations.AddField(
model_name="ceramiccache",
name="issuance_date",
field=models.DateTimeField(db_index=True, null=True),
),
]
7 changes: 7 additions & 0 deletions api/ceramic_cache/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ class ComposeDBSaveStatus(models.TextChoices):
db_index=True,
)

issuance_date = models.DateTimeField(
null=True, db_index=True
) # stamp['issuanceDate']
expiration_date = models.DateTimeField(
null=True, db_index=True
) # stamp['expirationDate']

class Meta:
unique_together = ["type", "address", "provider", "deleted_at"]

Expand Down
28 changes: 26 additions & 2 deletions api/passport_admin/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@

from django.contrib import admin

from .models import DismissedBanners, PassportBanner
from .models import (
DismissedBanners,
PassportBanner,
Notification,
NotificationStatus,
)

# admin.site.register(PassportBanner)
admin.site.register(DismissedBanners)
admin.site.register(NotificationStatus)


@admin.register(PassportBanner)
Expand All @@ -20,3 +25,22 @@ class PassportAdmin(admin.ModelAdmin):
list_display = ("content", "link", "is_active", "application")
search_fields = ("content", "is_active", "link")
list_filter = ("is_active", "application")


@admin.register(Notification)
class NotificationAdmin(admin.ModelAdmin):
"""
Admin class for Notification.
"""

list_display = ("notification_id", "type", "is_active", "eth_address")
search_fields = ("eth_address", "type")
list_filter = (
"type",
"eth_address",
"is_active",
"created_at",
"expires_at",
"link",
"link_text",
)
172 changes: 157 additions & 15 deletions api/passport_admin/api.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,35 @@
from typing import List, Optional

from ceramic_cache.api.v1 import JWTDidAuth
from django.db.models import Subquery
from ninja import Router, Schema
from django.db.models import Subquery, Q, OuterRef, Value
from django.db.models.functions import Coalesce
from ninja import Router
from passport_admin.schema import (
Banner,
GenericResponse,
NotificationSchema,
NotificationResponse,
NotificationPayload,
DismissPayload,
)
from passport_admin.notification_generators.deduplication_events import (
generate_deduplication_notifications,
)
from passport_admin.notification_generators.expired_stamp import (
generate_stamp_expired_notifications,
)
from passport_admin.notification_generators.on_chain_expired import (
generate_on_chain_expired_notifications,
)

from .models import DismissedBanners, PassportBanner

from .models import (
DismissedBanners,
PassportBanner,
Notification,
NotificationStatus,
)
from django.utils import timezone

router = Router()

Expand All @@ -14,13 +39,6 @@ def get_address(did: str):
return did[start:]


class Banner(Schema):
content: str
link: Optional[str] = None
banner_id: int
application: str = "passport"


@router.get(
"/banners",
response=List[Banner],
Expand Down Expand Up @@ -52,16 +70,12 @@ def get_banners(request, application: Optional[str] = "passport"):
)
for b in banners
]
except:
except PassportBanner.DoesNotExist:
return {
"status": "failed",
}


class GenericResponse(Schema):
status: str


@router.post(
"/banners/{banner_id}/dismiss",
response={200: GenericResponse},
Expand All @@ -82,3 +96,131 @@ def dismiss_banner(request, banner_id: int):
return {
"status": "failed",
}


@router.post(
"/notifications",
response=NotificationResponse,
auth=JWTDidAuth(),
)
def get_notifications(request, payload: NotificationPayload):
"""
Get all notifications for a specific address.
This also includes the generic notifications
"""
try:
address = get_address(request.auth.did)
current_date = timezone.now().date()

generate_deduplication_notifications(address=address)
generate_stamp_expired_notifications(address=address)
if payload.expired_chain_ids:
generate_on_chain_expired_notifications(
address=address, expired_chains=payload.expired_chain_ids
)

notification_status_subquery = NotificationStatus.objects.filter(
notification=OuterRef("pk"), eth_address=address
).values(
"is_read"
)[
:1
] # [:1] is used to limit the subquery to 1 result. There should be only 1 NotificationStatus per Notification

custom_notifications = (
Notification.objects.filter(
Q(is_active=True, eth_address=address)
& (Q(expires_at__gte=current_date) | Q(expires_at__isnull=True))
& (
Q(notificationstatus__is_deleted=False)
| Q(notificationstatus__isnull=True)
)
)
.annotate(
is_read=Coalesce(Subquery(notification_status_subquery), Value(False))
)
.order_by("-created_at")
)

general_notifications = (
Notification.objects.filter(
Q(is_active=True, eth_address=None)
& (Q(expires_at__gte=current_date) | Q(expires_at__isnull=True))
& (
Q(notificationstatus__is_deleted=False)
| Q(notificationstatus__isnull=True)
)
)
.annotate(
is_read=Coalesce(Subquery(notification_status_subquery), Value(False))
)
.order_by("-created_at")
)

all_notifications = sorted(
[
NotificationSchema(
notification_id=n.notification_id,
type=n.type,
content=n.content,
link=n.link,
link_text=n.link_text,
is_read=n.is_read,
created_at=n.created_at,
).dict()
for n in [*custom_notifications, *general_notifications]
],
key=lambda x: x["created_at"],
reverse=True,
)[:20] # Limit to the 20 newest notifications

return NotificationResponse(items=all_notifications).dict()

except Notification.DoesNotExist:
return {
"status": "failed",
}


@router.post(
"/notifications/{notification_id}",
response={200: GenericResponse},
auth=JWTDidAuth(),
)
def dismiss_notification(request, notification_id: str, payload: DismissPayload):
"""
Dismiss a notification
"""
try:
address = get_address(request.auth.did)
notification = Notification.objects.get(notification_id=notification_id)

if payload.dismissal_type not in ["read", "delete"]:
return {
"status": "Failed! Bad dismissal type.",
}

notification_status, created = NotificationStatus.objects.get_or_create(
eth_address=address, notification=notification
)

if payload.dismissal_type == "read":
if not notification_status.is_read:
notification_status.is_read = True
notification_status.save()
return {
"status": "success",
}

if payload.dismissal_type == "delete":
if not notification_status.is_deleted:
notification_status.is_deleted = True
notification_status.save()
return {
"status": "success",
}

except Notification.DoesNotExist:
return {
"status": "failed",
}

This file was deleted.

Loading
Loading