diff --git a/api/passport_admin/__init__.py b/api/passport_admin/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/passport_admin/admin.py b/api/passport_admin/admin.py new file mode 100644 index 000000000..9a44d1e94 --- /dev/null +++ b/api/passport_admin/admin.py @@ -0,0 +1,11 @@ +""" +Module for administering Passport Admin. +This includes registering the relevant models for PassportBanner and DismissedBanners. +""" + +from django.contrib import admin + +from .models import DismissedBanners, PassportBanner + +admin.site.register(PassportBanner) +admin.site.register(DismissedBanners) diff --git a/api/passport_admin/api.py b/api/passport_admin/api.py new file mode 100644 index 000000000..7e1ed5b8e --- /dev/null +++ b/api/passport_admin/api.py @@ -0,0 +1,74 @@ +from typing import List + +from ceramic_cache.api import JWTDidAuth +from django.db.models import Subquery +from ninja import Router, Schema + +from .models import DismissedBanners, PassportBanner + +router = Router() + + +def get_address(did: str): + start = did.index("0x") + return did[start:] + + +class Banner(Schema): + content: str + link: str + banner_id: int + + +@router.get( + "/banners", + response=List[Banner], + auth=JWTDidAuth(), +) +def get_banners(request): + """ + Get all banners + """ + try: + address = get_address(request.auth.did) + banners = ( + PassportBanner.objects.filter(is_active=True) + .exclude( + pk__in=Subquery( + DismissedBanners.objects.filter(address=address).values("banner_id") + ) + ) + .all() + ) + + return [Banner(content=b.content, link=b.link, banner_id=b.pk) for b in banners] + except: + return { + "status": "failed", + } + + +class GenericResponse(Schema): + status: str + + +@router.post( + "/banners/{banner_id}/dismiss", + response={200: GenericResponse}, + auth=JWTDidAuth(), +) +def dismiss_banner(request, banner_id: int): + """ + Dismiss a banner + """ + try: + banner = PassportBanner.objects.get(id=banner_id) + address = get_address(request.auth.did) + DismissedBanners.objects.create(address=address, banner=banner) + return { + "status": "success", + } + except PassportBanner.DoesNotExist: + return { + "status": "failed", + } diff --git a/api/passport_admin/apps.py b/api/passport_admin/apps.py new file mode 100644 index 000000000..2c8881251 --- /dev/null +++ b/api/passport_admin/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PassportAdminConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "passport_admin" diff --git a/api/passport_admin/migrations/0001_squashed_0003_remove_passportbanner_name.py b/api/passport_admin/migrations/0001_squashed_0003_remove_passportbanner_name.py new file mode 100644 index 000000000..ceb2d931e --- /dev/null +++ b/api/passport_admin/migrations/0001_squashed_0003_remove_passportbanner_name.py @@ -0,0 +1,61 @@ +# Generated by Django 4.2.3 on 2023-08-03 16:32 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + replaces = [ + ("passport_admin", "0001_initial"), + ("passport_admin", "0002_rename_description_passportbanner_content_and_more"), + ("passport_admin", "0003_remove_passportbanner_name"), + ] + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="PassportBanner", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("content", models.TextField()), + ("link", models.URLField(blank=True, null=True)), + ("is_active", models.BooleanField(default=True)), + ], + ), + migrations.CreateModel( + name="DismissedBanners", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("address", models.CharField(max_length=255)), + ( + "banner", + models.ForeignKey( + default=None, + on_delete=django.db.models.deletion.CASCADE, + related_name="dismissedbanners", + to="passport_admin.passportbanner", + ), + ), + ], + ), + ] diff --git a/api/passport_admin/migrations/__init__.py b/api/passport_admin/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/passport_admin/models.py b/api/passport_admin/models.py new file mode 100644 index 000000000..daeb09f1d --- /dev/null +++ b/api/passport_admin/models.py @@ -0,0 +1,30 @@ +""" +Module for defining models for Passport Admin. +Includes models for PassportBanner and DismissedBanners. +""" + +from django.db import models + + +class PassportBanner(models.Model): + """ + Model representing a Passport Banner. + """ + + content = models.TextField() + link = models.URLField(blank=True, null=True) + is_active = models.BooleanField(default=True) + + +class DismissedBanners(models.Model): + """ + Model representing Dismissed Banners. + """ + + address = models.CharField(max_length=255) + banner = models.ForeignKey( + PassportBanner, + on_delete=models.CASCADE, + default=None, + related_name="dismissedbanners", + ) diff --git a/api/passport_admin/tests/conftest.py b/api/passport_admin/tests/conftest.py new file mode 100644 index 000000000..b8e05e86f --- /dev/null +++ b/api/passport_admin/tests/conftest.py @@ -0,0 +1,7 @@ +from scorer.test.conftest import ( + api_key, + sample_address, + sample_provider, + sample_token, + verifiable_credential, +) diff --git a/api/passport_admin/tests/test_passport_admin.py b/api/passport_admin/tests/test_passport_admin.py new file mode 100644 index 000000000..847458a91 --- /dev/null +++ b/api/passport_admin/tests/test_passport_admin.py @@ -0,0 +1,47 @@ +import pytest +from django.test import Client +from passport_admin.api import get_address +from passport_admin.models import DismissedBanners, PassportBanner + +pytestmark = pytest.mark.django_db + +client = Client() + + +class TestPassPortAdmin: + def test_get_address(self): + assert ( + get_address("did:pkh:eip155:1:0xc79abb54e4824cdb65c71f2eeb2d7f2db5da1fb8") + == "0xc79abb54e4824cdb65c71f2eeb2d7f2db5da1fb8" + ) + + def test_get_banners(self, sample_token): + response = client.get( + "/passport-admin/banners", + HTTP_AUTHORIZATION=f"Bearer {sample_token}", + content_type="application/json", + ) + assert response.json() == [] + + def test_dismiss_banner(self, sample_token, sample_address): + banner = PassportBanner.objects.create(content="test", link="test") + response = client.post( + f"/passport-admin/banners/{banner.pk}/dismiss", + {}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Bearer {sample_token}", + ) + assert response.json() == {"status": "success"} + dismissed_banner = DismissedBanners.objects.get( + banner=banner, address=sample_address.lower() + ) + assert dismissed_banner.banner == banner + + def test_dismiss_banner_does_not_exist(self, sample_token): + response = client.post( + "/passport-admin/banners/1/dismiss", + {}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Bearer {sample_token}", + ) # Needs JWT Auth + assert response.json() == {"status": "failed"} diff --git a/api/scorer/api.py b/api/scorer/api.py index 4a3d1c071..49da74063 100644 --- a/api/scorer/api.py +++ b/api/scorer/api.py @@ -9,6 +9,7 @@ from ninja.openapi.schema import OpenAPISchema from ninja.operation import Operation from ninja.types import DictStrAny +from passport_admin.api import router as passport_admin_router from registry.api.v1 import analytics_router, feature_flag_router from registry.api.v1 import router as registry_router_v1 from registry.api.v2 import router as registry_router_v2 @@ -89,5 +90,8 @@ def service_unavailable(request, _): ceramic_cache_api = NinjaAPI(urls_namespace="ceramic-cache", docs_url=None) ceramic_cache_api.add_router("", ceramic_cache_router) +passport_admin_api = NinjaAPI(urls_namespace="passport-admin", docs_url=None) +passport_admin_api.add_router("", passport_admin_router) + analytics_api = NinjaAPI(urls_namespace="analytics", title="Data Analytics API") analytics_api.add_router("", analytics_router) diff --git a/api/scorer/settings/base.py b/api/scorer/settings/base.py index a8aaa4d61..a7933e241 100644 --- a/api/scorer/settings/base.py +++ b/api/scorer/settings/base.py @@ -81,6 +81,7 @@ "account", "ninja_extra", "social_django", + "passport_admin", # "debug_toolbar", "cgrants", "django_filters", diff --git a/api/scorer/urls.py b/api/scorer/urls.py index bd81589b6..bae5f4e74 100644 --- a/api/scorer/urls.py +++ b/api/scorer/urls.py @@ -23,6 +23,7 @@ analytics_api, ceramic_cache_api, feature_flag_api, + passport_admin_api, registry_api_v1, registry_api_v2, ) @@ -43,5 +44,6 @@ path("admin/", admin.site.urls), path("account/", include("account.urls")), path("social/", include("social_django.urls", namespace="social")), + path("passport-admin/", passport_admin_api.urls), # path("__debug__/", include("debug_toolbar.urls")), ]