diff --git a/backend/src/module/api/notification.py b/backend/src/module/api/notification.py index ffe184d0..7b18fca3 100644 --- a/backend/src/module/api/notification.py +++ b/backend/src/module/api/notification.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, Query from module.conf.config import settings from module.notification import Notifier @@ -14,8 +14,25 @@ def get_notifier(): ) +@router.get("/total") +async def get_total_notification(notifier: Notifier = Depends(get_notifier)): + cursor = notifier.q.conn.cursor() + stmt = """SELECT COUNT(*) FROM Queue WHERE status=0""" + + try: + total = cursor.execute(stmt).fetchone()[0] + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + return dict(code=0, msg="success", data=dict(total=total)) + + @router.get("") -async def get_notification(notifier: Notifier = Depends(get_notifier)): +async def get_notification( + page: int = Query(1, ge=1), + limit: int = Query(20, ge=10, le=20, description="max limit is 20 per page"), + notifier: Notifier = Depends(get_notifier), +): cursor = notifier.q.conn.cursor() stmt = r""" SELECT message_id, data, in_time as datetime, status as has_read @@ -24,6 +41,9 @@ async def get_notification(notifier: Notifier = Depends(get_notifier)): ORDER BY in_time DESC """ + offset = (page - 1) * limit + stmt += f"LIMIT {limit} OFFSET {offset}" + try: rows = cursor.execute(stmt).fetchall() except Exception as e: diff --git a/backend/src/test/api/test_notificaion.py b/backend/src/test/api/test_notificaion.py index f9c33e8e..441c144e 100644 --- a/backend/src/test/api/test_notificaion.py +++ b/backend/src/test/api/test_notificaion.py @@ -12,6 +12,43 @@ class TestNotificationAPI: def setup_class(cls): cls.content = NotificationContent(content="fooo") + @pytest.mark.asyncio + async def test_get_total_notification( + self, aclient: AsyncClient, mocker: MockerFixture + ): + mocked_db = sqlite3.connect(":memory:") + mocked_db.execute( + "CREATE TABLE Queue (message_id TEXT, data TEXT, in_time INT, status INT)" + ) + mocked_db.execute( + "INSERT INTO Queue (message_id, data, in_time, status) VALUES (?, ?, ?, ?)", + ("foo", "bar", 123, 0), + ) + mocked_db.commit() + + m = mocker.patch.object(Notifier, "q", return_value=object()) + m.conn = mocked_db + + resp = await aclient.get("/v1/notification/total") + assert resp.status_code == 200 + assert resp.json() == dict(code=0, msg="success", data=dict(total=1)) + + @pytest.mark.asyncio + async def test_get_total_notification_with_exception( + self, aclient: AsyncClient, mocker: MockerFixture + ): + mocked_conn = mocker.MagicMock() + mocked_cursor = mocker.MagicMock() + mocked_conn.cursor.return_value = mocked_cursor + mocked_cursor.execute.side_effect = Exception("unknown error") + + m = mocker.patch.object(Notifier, "q", return_value=object()) + m.conn = mocked_conn + + resp = await aclient.get("/v1/notification/total") + assert resp.status_code == 500 + assert resp.json() == dict(detail="unknown error") + @pytest.mark.asyncio async def test_get_notification(self, aclient: AsyncClient, mocker: MockerFixture): mocked_db = sqlite3.connect(":memory:") @@ -27,7 +64,7 @@ async def test_get_notification(self, aclient: AsyncClient, mocker: MockerFixtur m = mocker.patch.object(Notifier, "q", return_value=object()) m.conn = mocked_db - resp = await aclient.get("/v1/notification") + resp = await aclient.get("/v1/notification", params={"page": 1, "limit": 20}) assert resp.status_code == 200 assert resp.json() == dict( code=0, diff --git a/webui/src/api/notification.ts b/webui/src/api/notification.ts index 6e728bc5..1c5d1b2f 100644 --- a/webui/src/api/notification.ts +++ b/webui/src/api/notification.ts @@ -1,6 +1,16 @@ export const apiNotification = { - async get() { - const { data } = await axios.get('api/v1/notification'); + async getTotal() { + const { data } = await axios.get('api/v1/notification/total'); + return data; + }, + + async get({ page, limit }: { page?: number; limit?: number }) { + const { data } = await axios.get('api/v1/notification', { + params: { + page, + limit, + }, + }); return data; }, diff --git a/webui/src/components/ab-notification.vue b/webui/src/components/ab-notification.vue index e399bc2f..ee780e73 100644 --- a/webui/src/components/ab-notification.vue +++ b/webui/src/components/ab-notification.vue @@ -27,7 +27,7 @@ onUnmounted(() => { diff --git a/webui/src/hooks/useNotificaiton.ts b/webui/src/hooks/useNotificaiton.ts index 3b4bcbba..b7552022 100644 --- a/webui/src/hooks/useNotificaiton.ts +++ b/webui/src/hooks/useNotificaiton.ts @@ -3,21 +3,29 @@ import type { Notification } from '#/notification'; export const useNotification = createSharedComposable(() => { // TODO: add auth // const { auth } = useAuth(); + const notifications = ref([]); const total = ref(0); + function getTotal() { + const { execute, onResult } = useApi(apiNotification.getTotal, {}); + + onResult((res) => { + total.value = res.data.total; + }); + execute(); + } + function getNotification() { const { execute, onResult } = useApi(apiNotification.get, {}); onResult((res) => { - total.value = res.data.total; - notifications.value = res.data.messages.map((item, index) => { + notifications.value = res.data.messages.map((item) => { const { content } = JSON.parse(item.data); const value = { - key: index, id: item.message_id, title: 'AutoBangumi', - hasRead: false, + has_read: Boolean(item.has_read), datetime: `${item.datetime}`, content, }; @@ -29,11 +37,14 @@ export const useNotification = createSharedComposable(() => { // if (auth.value !== '') { // execute(); // } - execute(); + execute({ page: 1, limit: 10 }); } const { pause: offUpdate, resume: onUpdate } = useIntervalFn( - getNotification, + () => { + getTotal(); + getNotification(); + }, 5000, { immediate: false, @@ -48,3 +59,45 @@ export const useNotification = createSharedComposable(() => { offUpdate, }; }); + +export const useNotificationPage = createSharedComposable(() => { + // TODO: add auth + // const { auth } = useAuth(); + + const { total } = useNotification(); + const { execute, onResult } = useApi(apiNotification.get, {}); + const notifications = ref([]); + const page = ref(1); + const limit = ref(10); + + onResult((res) => { + notifications.value = res.data.messages.map((item) => { + const { content } = JSON.parse(item.data); + const value = { + id: item.message_id, + title: 'AutoBangumi', + datetime: `${item.datetime}`, + content, + }; + return value; + }); + }); + + // TODO: add auth + // if (auth.value !== '') { + // execute(); + // } + + watch([page, limit], () => { + execute({ page: page.value, limit: limit.value }); + }); + + execute({ page: page.value, limit: limit.value }); + + return { + total, + page, + limit, + notifications, + }; +}); diff --git a/webui/src/pages/index/notification.vue b/webui/src/pages/index/notification.vue index a1d47515..ba7a9e45 100644 --- a/webui/src/pages/index/notification.vue +++ b/webui/src/pages/index/notification.vue @@ -1,17 +1,18 @@ diff --git a/webui/types/dts/auto-imports.d.ts b/webui/types/dts/auto-imports.d.ts index e3f4609d..d6990c44 100644 --- a/webui/types/dts/auto-imports.d.ts +++ b/webui/types/dts/auto-imports.d.ts @@ -222,6 +222,7 @@ declare global { const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage'] const useNetwork: typeof import('@vueuse/core')['useNetwork'] const useNotification: typeof import('../../src/hooks/useNotificaiton')['useNotification'] + const useNotificationPage: typeof import('../../src/hooks/useNotificaiton')['useNotificationPage'] const useNow: typeof import('@vueuse/core')['useNow'] const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl'] const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination'] diff --git a/webui/types/notification.ts b/webui/types/notification.ts index 0289a423..41de6bfc 100644 --- a/webui/types/notification.ts +++ b/webui/types/notification.ts @@ -4,5 +4,5 @@ export interface Notification { title: string; content: string; datetime: string; - hasRead: boolean; + has_read: boolean; }