From dd5769823f3598f817e3c958b814ccda304c41c5 Mon Sep 17 00:00:00 2001 From: edraft Date: Sat, 14 Dec 2024 19:19:27 +0100 Subject: [PATCH] Handle visits --- api/src/api/routes/redirect.py | 17 ++++++++- api/src/api_graphql/graphql/short_url.gql | 1 + .../api_graphql/queries/short_url_query.py | 1 + api/src/data/schemas/public/short_url.py | 6 +++ .../data/schemas/public/short_url_visit.py | 37 +++++++++++++++++++ .../schemas/public/short_url_visit_dao.py | 25 +++++++++++++ .../2024-12-13-22-40-short-url-vistis.sql | 23 ++++++++++++ web/src/app/model/entities/short-url.ts | 1 + .../short-urls/short-urls.data.service.ts | 2 + 9 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 api/src/data/schemas/public/short_url_visit.py create mode 100644 api/src/data/schemas/public/short_url_visit_dao.py create mode 100644 api/src/data/scripts/2024-12-13-22-40-short-url-vistis.sql diff --git a/api/src/api/routes/redirect.py b/api/src/api/routes/redirect.py index 44af696..c6932a3 100644 --- a/api/src/api/routes/redirect.py +++ b/api/src/api/routes/redirect.py @@ -1,9 +1,13 @@ -from flask import redirect, jsonify +import json + +from flask import redirect, jsonify, request from api.route import Route from core.logger import Logger from data.schemas.public.short_url import ShortUrl from data.schemas.public.short_url_dao import shortUrlDao +from data.schemas.public.short_url_visit import ShortUrlVisit +from data.schemas.public.short_url_visit_dao import shortUrlVisitDao logger = Logger(__name__) @@ -26,4 +30,15 @@ async def handle_short_url(path: str): if from_db is None: return {"error": "Short URL not found"}, 404 + try: + await shortUrlVisitDao.create( + ShortUrlVisit( + 0, + from_db.id, + request.headers.get("User-Agent") + ) + ) + except Exception as e: + logger.error(f"Failed to update short url {from_db.short_url} with error", e) + return redirect(from_db.target_url) diff --git a/api/src/api_graphql/graphql/short_url.gql b/api/src/api_graphql/graphql/short_url.gql index f7204fc..90fee2d 100644 --- a/api/src/api_graphql/graphql/short_url.gql +++ b/api/src/api_graphql/graphql/short_url.gql @@ -9,6 +9,7 @@ type ShortUrl implements DbModel { shortUrl: String targetUrl: String description: String + visits: Int group: Group deleted: Boolean diff --git a/api/src/api_graphql/queries/short_url_query.py b/api/src/api_graphql/queries/short_url_query.py index 87c2804..592f934 100644 --- a/api/src/api_graphql/queries/short_url_query.py +++ b/api/src/api_graphql/queries/short_url_query.py @@ -9,3 +9,4 @@ class ShortUrlQuery(DbModelQueryABC): self.set_field("targetUrl", lambda x, *_: x.target_url) self.set_field("description", lambda x, *_: x.description) self.set_field("group", lambda x, *_: x.group) + self.set_field("visits", lambda x, *_: x.visit_count) diff --git a/api/src/data/schemas/public/short_url.py b/api/src/data/schemas/public/short_url.py index f87cf4b..ab7cf23 100644 --- a/api/src/data/schemas/public/short_url.py +++ b/api/src/data/schemas/public/short_url.py @@ -68,6 +68,12 @@ class ShortUrl(DbModelABC): return await groupDao.get_by_id(self._group_id) + @async_property + async def visit_count(self) -> int: + from data.schemas.public.short_url_visit_dao import shortUrlVisitDao + + return await shortUrlVisitDao.count_by_id(self.id) + def to_dto(self) -> dict: return { "id": self.id, diff --git a/api/src/data/schemas/public/short_url_visit.py b/api/src/data/schemas/public/short_url_visit.py new file mode 100644 index 0000000..b4a37b3 --- /dev/null +++ b/api/src/data/schemas/public/short_url_visit.py @@ -0,0 +1,37 @@ +from datetime import datetime +from typing import Optional + +from async_property import async_property + +from core.database.abc.db_model_abc import DbModelABC +from core.typing import SerialId +from data.schemas.public.short_url import ShortUrl +from data.schemas.public.short_url_dao import shortUrlDao + + +class ShortUrlVisit(DbModelABC): + def __init__( + self, + id: SerialId, + short_url_id: SerialId, + client: Optional[str], + deleted: bool = False, + editor_id: Optional[SerialId] = None, + created: Optional[datetime] = None, + updated: Optional[datetime] = None, + ): + DbModelABC.__init__(self, id, deleted, editor_id, created, updated) + self._short_url_id = short_url_id + self._client = client + + @property + def short_url_id(self) -> SerialId: + return self._short_url_id + + @async_property + async def short_url(self) -> ShortUrl: + return await shortUrlDao.get_by_id(self._short_url_id) + + @property + def client(self) -> Optional[str]: + return self._client diff --git a/api/src/data/schemas/public/short_url_visit_dao.py b/api/src/data/schemas/public/short_url_visit_dao.py new file mode 100644 index 0000000..99f72a5 --- /dev/null +++ b/api/src/data/schemas/public/short_url_visit_dao.py @@ -0,0 +1,25 @@ +from core.logger import DBLogger +from data.schemas.public.short_url import ShortUrl +from data.schemas.public.short_url_visit import ShortUrlVisit + +logger = DBLogger(__name__) + +from core.database.abc.db_model_dao_abc import DbModelDaoABC + + +class ShortUrlVisitDao(DbModelDaoABC[ShortUrlVisit]): + def __init__(self): + DbModelDaoABC.__init__(self, __name__, ShortUrl, "public.short_url_visits") + self.attribute(ShortUrlVisit.short_url_id, int) + self.attribute(ShortUrlVisit.client, str) + + async def count_by_id(self, fid: int) -> int: + result = await self._db.select_map( + f"SELECT COUNT(*) FROM {self._table_name} WHERE shortUrlId = {fid}" + ) + if result is None or len(result) == 0: + return 0 + return result[0]["count"] + + +shortUrlVisitDao = ShortUrlVisitDao() diff --git a/api/src/data/scripts/2024-12-13-22-40-short-url-vistis.sql b/api/src/data/scripts/2024-12-13-22-40-short-url-vistis.sql new file mode 100644 index 0000000..9d28357 --- /dev/null +++ b/api/src/data/scripts/2024-12-13-22-40-short-url-vistis.sql @@ -0,0 +1,23 @@ +CREATE TABLE IF NOT EXISTS public.short_url_visits +( + Id SERIAL PRIMARY KEY, + ShortUrlId INT NOT NULL REFERENCES public.short_urls (Id), + Client TEXT NOT NULL, + -- for history + Deleted BOOLEAN NOT NULL DEFAULT FALSE, + EditorId INT NULL REFERENCES administration.users (Id), + CreatedUtc timestamptz NOT NULL DEFAULT NOW(), + UpdatedUtc timestamptz NOT NULL DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS public.short_url_visits_history +( + LIKE public.short_url_visits +); + +CREATE TRIGGER short_url_visits_history_trigger + BEFORE INSERT OR UPDATE OR DELETE + ON public.short_url_visits + FOR EACH ROW +EXECUTE FUNCTION public.history_trigger_function(); + diff --git a/web/src/app/model/entities/short-url.ts b/web/src/app/model/entities/short-url.ts index 466d881..90d79b7 100644 --- a/web/src/app/model/entities/short-url.ts +++ b/web/src/app/model/entities/short-url.ts @@ -6,6 +6,7 @@ export interface ShortUrl extends DbModel { shortUrl: string; targetUrl: string; description: string; + visits: number; group?: Group; } diff --git a/web/src/app/modules/admin/short-urls/short-urls.data.service.ts b/web/src/app/modules/admin/short-urls/short-urls.data.service.ts index d7e47a5..cd40395 100644 --- a/web/src/app/modules/admin/short-urls/short-urls.data.service.ts +++ b/web/src/app/modules/admin/short-urls/short-urls.data.service.ts @@ -60,6 +60,7 @@ export class ShortUrlsDataService shortUrl targetUrl description + visits group { id name @@ -98,6 +99,7 @@ export class ShortUrlsDataService shortUrl targetUrl description + visits group { id name