From d81d18dcc3eb9955941af88dbe9deb68ad093633 Mon Sep 17 00:00:00 2001 From: DmsAnhr Date: Mon, 24 Nov 2025 08:57:43 +0700 Subject: [PATCH] fixing saving data model --- api/routers/datasets_router.py | 75 ++- api/routers/upload_file_router.py | 6 +- core/config.py | 2 + database/connection.py | 4 +- main.py | 52 +- .../__pycache__/reader_csv.cpython-39.pyc | Bin 4085 -> 0 bytes .../__pycache__/reader_gdb.cpython-39.pyc | Bin 1862 -> 0 bytes .../__pycache__/reader_mpk.cpython-39.pyc | Bin 2263 -> 0 bytes .../__pycache__/reader_pdf.cpython-39.pyc | Bin 5956 -> 0 bytes .../__pycache__/reader_shp.cpython-39.pyc | Bin 1641 -> 0 bytes services/upload_file/upload.py | 457 +++++------------- .../geometry_detector.cpython-39.pyc | Bin 13831 -> 0 bytes .../__pycache__/pdf_cleaner.cpython-39.pyc | Bin 5960 -> 0 bytes utils/logger_config.py | 33 ++ 14 files changed, 276 insertions(+), 353 deletions(-) delete mode 100644 services/upload_file/readers/__pycache__/reader_csv.cpython-39.pyc delete mode 100644 services/upload_file/readers/__pycache__/reader_gdb.cpython-39.pyc delete mode 100644 services/upload_file/readers/__pycache__/reader_mpk.cpython-39.pyc delete mode 100644 services/upload_file/readers/__pycache__/reader_pdf.cpython-39.pyc delete mode 100644 services/upload_file/readers/__pycache__/reader_shp.cpython-39.pyc delete mode 100644 services/upload_file/utils/__pycache__/geometry_detector.cpython-39.pyc delete mode 100644 services/upload_file/utils/__pycache__/pdf_cleaner.cpython-39.pyc diff --git a/api/routers/datasets_router.py b/api/routers/datasets_router.py index a88282f..6d2d61a 100644 --- a/api/routers/datasets_router.py +++ b/api/routers/datasets_router.py @@ -1,11 +1,82 @@ from fastapi import APIRouter -from core.config import engine +from sqlalchemy import text +from database.connection import engine from services.datasets.delete import delete_dataset_from_partition # import fungsi di atas from response import successRes, errorRes router = APIRouter() -@router.delete("/dataset/{user_id}/{metadata_id}") +def serialize_row(row_dict): + new_dict = {} + for key, value in row_dict.items(): + if hasattr(value, "isoformat"): + new_dict[key] = value.isoformat() + else: + new_dict[key] = value + return new_dict + + + + +@router.get("/metadata") +async def get_author_metadata( + # user = Depends(get_current_user) +): + """ + Mengambil data author_metadata: + - Admin → semua data + - User → hanya data miliknya + """ + + try: + async with engine.begin() as conn: + # if user.role == "admin": + # query = text(""" + # SELECT * + # FROM backend.author_metadata + # ORDER BY created_at DESC + # """) + # result = await conn.execute(query) + + # else: + # query = text(""" + # SELECT * + # FROM backend.author_metadata + # WHERE user_id = :uid + # ORDER BY created_at DESC + # """) + # result = await conn.execute(query, {"uid": user.id}) + + # rows = result.fetchall() + + query = text(""" + SELECT * + FROM backend.author_metadata + ORDER BY created_at DESC + """) + result = await conn.execute(query) + rows = result.fetchall() + + + # data = [dict(row._mapping) for row in rows] + data = [serialize_row(dict(row._mapping)) for row in rows] + + return successRes( + message="Berhasil mengambil data author metadata", + data=data + ) + + except Exception as e: + print(f"[ERROR] Gagal ambil author_metadata: {e}") + raise errorRes( + status_code=500, + message="Gagal mengambil data author_metadata", + details=str(e) + ) + + + +@router.delete("/delete/{user_id}/{metadata_id}") async def delete_dataset(user_id: int, metadata_id: int, title: str): """ Hapus dataset tertentu (berdasarkan user_id dan metadata_id) diff --git a/api/routers/upload_file_router.py b/api/routers/upload_file_router.py index dbcf773..6924278 100644 --- a/api/routers/upload_file_router.py +++ b/api/routers/upload_file_router.py @@ -1,7 +1,7 @@ from fastapi import APIRouter, File, Form, UploadFile, Depends from pydantic import BaseModel -from typing import List, Optional +from typing import Any, Dict, List, Optional from services.upload_file.upload import handle_upload_file, handle_process_pdf, handle_to_postgis from api.deps.role_dependency import require_role from database.connection import engine @@ -30,7 +30,9 @@ class UploadRequest(BaseModel): title: str rows: List[dict] columns: List[str] + author: Dict[str, Any] @router.post("/to-postgis") async def upload_to_postgis(payload: UploadRequest): - return await handle_to_postgis(payload, engine) \ No newline at end of file + # return await handle_to_postgis(payload, engine) + return await handle_to_postgis(payload) \ No newline at end of file diff --git a/core/config.py b/core/config.py index 4889546..4a8258a 100644 --- a/core/config.py +++ b/core/config.py @@ -7,6 +7,7 @@ load_dotenv() API_VERSION = "2.1.3" POSTGIS_URL = os.getenv("POSTGIS_URL") +POSTGIS_SYNC_URL = os.getenv("SYNC_URL") UPLOAD_FOLDER = Path(os.getenv("UPLOAD_FOLDER", "./uploads")) MAX_FILE_MB = int(os.getenv("MAX_FILE_MB", 30)) @@ -18,6 +19,7 @@ ALLOWED_ORIGINS = [ "192.168.60.24:5173", "http://labai.polinema.ac.id:666", + "https://kkqc31ns-5173.asse.devtunnels.ms" ] REFERENCE_DB_URL = os.getenv("REFERENCE_DB_URL") diff --git a/database/connection.py b/database/connection.py index 921a694..710396f 100644 --- a/database/connection.py +++ b/database/connection.py @@ -2,8 +2,10 @@ from sqlalchemy import create_engine from sqlalchemy.ext.asyncio import create_async_engine from sqlalchemy.orm import sessionmaker from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker -from core.config import POSTGIS_URL +from core.config import POSTGIS_URL, POSTGIS_SYNC_URL engine = create_async_engine(POSTGIS_URL, pool_pre_ping=True) # SessionLocal = sessionmaker(bind=engine) SessionLocal = async_sessionmaker(engine, expire_on_commit=False) + +sync_engine = create_engine(POSTGIS_SYNC_URL) diff --git a/main.py b/main.py index e3092a9..4b8c7bc 100644 --- a/main.py +++ b/main.py @@ -6,8 +6,9 @@ from database.models import Base from api.routers.system_router import router as system_router from api.routers.upload_file_router import router as upload_router from api.routers.auth_router import router as auth_router -from contextlib import asynccontextmanager -from utils.qgis_init import init_qgis +from api.routers.datasets_router import router as dataset_router +# from contextlib import asynccontextmanager +# from utils.qgis_init import init_qgis app = FastAPI( title="ETL Geo Upload Service", @@ -26,35 +27,36 @@ app.add_middleware( # Base.metadata.create_all(bind=engine) # qgis setup -@asynccontextmanager -async def lifespan(app: FastAPI): - global qgs - qgs = init_qgis() - print("QGIS initialized") +# @asynccontextmanager +# async def lifespan(app: FastAPI): +# global qgs +# qgs = init_qgis() +# print("QGIS initialized") - yield +# yield - # SHUTDOWN (optional) - print("Shutting down...") +# # SHUTDOWN (optional) +# print("Shutting down...") -app = FastAPI(lifespan=lifespan) +# app = FastAPI(lifespan=lifespan) -@app.get("/qgis/status") -def qgis_status(): - try: - version = QgsApplication.qgisVersion() - return { - "qgis_status": "connected", - "qgis_version": version - } - except Exception as e: - return { - "qgis_status": "error", - "error": str(e) - } +# @app.get("/qgis/status") +# def qgis_status(): +# try: +# version = QgsApplication.qgisVersion() +# return { +# "qgis_status": "connected", +# "qgis_version": version +# } +# except Exception as e: +# return { +# "qgis_status": "error", +# "error": str(e) +# } # Register routers app.include_router(system_router, tags=["System"]) app.include_router(auth_router, prefix="/auth", tags=["Auth"]) -app.include_router(upload_router, prefix="/upload", tags=["Upload"]) \ No newline at end of file +app.include_router(upload_router, prefix="/upload", tags=["Upload"]) +app.include_router(dataset_router, prefix="/dataset", tags=["Upload"]) \ No newline at end of file diff --git a/services/upload_file/readers/__pycache__/reader_csv.cpython-39.pyc b/services/upload_file/readers/__pycache__/reader_csv.cpython-39.pyc deleted file mode 100644 index 28912198b0358e0ed202448cb77150f1b39c1ea9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4085 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 begT*nh2l$OvXbFl*ozDMEQ{GX)N20+UqU{e diff --git a/services/upload_file/readers/__pycache__/reader_pdf.cpython-39.pyc b/services/upload_file/readers/__pycache__/reader_pdf.cpython-39.pyc deleted file mode 100644 index 4c165af49ea8bf308659c7d70507dd4c84b29cbc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5956 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 b+ZV7=$-yIGyF^DmIP@v$rIk{p_QwAK#}XPj diff --git a/services/upload_file/readers/__pycache__/reader_shp.cpython-39.pyc b/services/upload_file/readers/__pycache__/reader_shp.cpython-39.pyc deleted file mode 100644 index 0d13461c0d4f52354f381db4f87710e2a56a1119..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1641 zcmZWp&2Jk;6rb5$+q)abaZ8~|N?}zYAQw9=Q4dvB@sTtFi4f6`s9Pdy;u+iP>{n*S zZDY-HXt-9*9gx;Xu81>#1Sd3CPW%Hs^u(KW3X#~+zV~L{oA-XdH?vC?7kvaz>rOLz zRY&MAGdcg*F!>Z-u>*o4ienVwN?V~-X*;wl?SxLHYhkU@^{@`wiW`X=x)?E!+CO3H z(Ap19=u>ka>zWvmoPpuwM2`B{te-t?&<5fc@QT;qQAji;06D?W0Z3t9 zOQ90aP-#(XhWQ`TE}a>8WUZ_#MD3aNa<#CPT{aY+VT|^Ghy&xq#wcxHN8;f+!qs>L za)=5?x}|4gH_`sh@9d&h)Qg5{!0f}qRqk0W2Wlzhl)iMw3uRN)lskj^7#*pjR5{{T zKv^>=l|$<*fKo2fMitvq5cebc**byf)KTD@w&YUTHtU7AhtdrP$qT3Wcp0O@$7s3= zxm@MX_dLgY=n-%z{O|qLpX+tBj;N=+@x>?j6pIb8-Wp#j-=3jU%zuZ24REyMGF_B}rJhxA93ddWtV736*msCqFQSUZV&C*v(hUF|sIJQ(idi5dN)CgAEM}wNFcVoi>`a}`G|DyRV2DvZ{pfxb zLnFzO$stLDYNK%lGzut*NfLx#?smAjcGD}5H@7~2u)QCA&5{GsC;!#D6--yQ$&kcB z@_(0hhb&8&1sT(Rc0lIa)8QlyCNW8e0D(sUN=`VqQb|Ug zPDlGW&vM3P#KfA#YXD8#eJ-Zn*6zLSTN|I;xOpGGmqwK4G3m3l_qG3!#1po~dB$}u z&Ym)^U6xYuG?F9j0{7hT){W$lnu%^eljbX7l;1TjpFNYD^d*U7ZDm3`Pf2{J>m<)1 z4>x}T?ToW1)wP^QsnqTP5iBJM)Alf@+T)DSUX{|@de&#TjIvZW`dK_lQZc9CMxqDB z<*~L+mD&J8h<1Q!$}Ll%30>PHlHBExmp9B(LonF`>blOJMM8oWqlt`SUFV79jIFgR zY++p{IgNPe!pZi`f25+=<7^OmX2~D}QasC~#ywNp%<%xw(}BjoLh!zJ2aLc;FcCV& z(vCS-!#K;uFNk*m(d%!z4+P_)n~;8YmuF+vm!dn#;|%y`GE0bI-i44SQJ?+mG4;Bp zM`r6>_W)NXKh|#5I+%EnUVH@tVF$aGgB`1dUEIPSgMF#8mcepiEZdG{fv1f>uvQ_q zX?9?+V6?0oR?})(m#}Z~>yT}2L3=&3EhMjURXsQ-*3BmOc9PLa% str: - """Mengubah judul dataset jadi nama aman untuk VIEW""" - return re.sub(r'[^a-zA-Z0-9]+', '_', value.lower()).strip('_') - - -# async def create_dataset_view_from_metadata(conn, metadata_id: int, user_id: int, title: str): -# """Membuat VIEW PostgreSQL berdasarkan metadata dataset dan registrasi geometry untuk QGIS.""" -# norm_title = slugify(title) -# view_name = f"v_user_{user_id}_{norm_title}" -# base_table = f"test_partition_user_{user_id}" - -# # 1️⃣ Hapus view lama jika ada -# drop_query = text(f"DROP VIEW IF EXISTS {view_name} CASCADE;") -# await conn.execute(drop_query) - -# # 2️⃣ Buat view baru -# create_view_query = text(f""" -# CREATE OR REPLACE VIEW {view_name} AS -# SELECT p.*, m.title, m.year, m.description -# FROM {base_table} p -# JOIN dataset_metadata m ON m.id = p.metadata_id -# WHERE p.metadata_id = {metadata_id}; -# """) -# await conn.execute(create_view_query) - -# # 3️⃣ Daftarkan geometry column agar QGIS mengenali layer ini -# # (gunakan Populate_Geometry_Columns jika PostGIS >= 3) -# populate_query = text(f"SELECT Populate_Geometry_Columns('{view_name}'::regclass);") -# await conn.execute(populate_query) - -# print(f"[INFO] VIEW {view_name} berhasil dibuat dan geometry terdaftar.") - - -async def create_dataset_view_from_metadata(conn, metadata_id: int, user_id: int, title: str): - """Buat VIEW dinamis sesuai struktur atribut JSON pada dataset (hindari duplikasi nama kolom).""" - norm_title = slugify(title) - view_name = f"v_user_{user_id}_{norm_title}" - base_table = f"test_partition_user_{user_id}" - - # Ambil daftar field dari metadata - result = await conn.execute(text("SELECT fields FROM dataset_metadata WHERE id=:mid"), {"mid": metadata_id}) - fields_json = result.scalar_one_or_none() - - # --- daftar kolom bawaan dari tabel utama - base_columns = {"id", "user_id", "metadata_id", "geom"} - - columns_sql = "" - field_list = [] - - if fields_json: +def str_to_date(raw_date: str): + if raw_date: try: - # handle jika data sudah berupa list atau string JSON - if isinstance(fields_json, str): - field_list = json.loads(fields_json) - elif isinstance(fields_json, list): - field_list = fields_json - else: - raise ValueError(f"Tipe data fields_json tidak dikenali: {type(fields_json)}") - - for f in field_list: - safe_col = slugify(f) - # Hindari duplikat nama dengan kolom utama - if safe_col in base_columns: - alias_name = f"attr_{safe_col}" - else: - alias_name = safe_col - - columns_sql += f", p.attributes->>'{f}' AS {alias_name}" - + return datetime.strptime(raw_date, "%Y-%m-%d").date() except Exception as e: - print(f"[WARN] Gagal parse field list metadata: {e}") + print("[WARNING] Tidak bisa parse dateCreated:", e) + return None - # 1️⃣ Drop view lama - await conn.execute(text(f"DROP VIEW IF EXISTS {view_name} CASCADE;")) - - # 2️⃣ Buat view baru dinamis - create_view_query = f""" - CREATE OR REPLACE VIEW {view_name} AS - SELECT p.id, p.user_id, p.metadata_id, p.geom - {columns_sql}, - m.title, m.year, m.description - FROM {base_table} p - JOIN dataset_metadata m ON m.id = p.metadata_id - WHERE p.metadata_id = {metadata_id}; - """ - await conn.execute(text(create_view_query)) - - # 3️⃣ Register geometry untuk QGIS - await conn.execute(text(f"SELECT Populate_Geometry_Columns('{view_name}'::regclass);")) - - print(f"[INFO] VIEW {view_name} berhasil dibuat (kolom dinamis: {field_list if field_list else '(none)'}).") - - -async def handle_to_postgis(payload, engine, user_id: int = 3): - """ - Menangani upload data spasial ke PostGIS (dengan partition per user). - - Jika partisi belum ada, akan dibuat otomatis - - Metadata dataset disimpan di tabel dataset_metadata - - Data spasial dimasukkan ke tabel partisi (test_partition_user_{id}) - - VIEW otomatis dibuat untuk QGIS - """ +import asyncio +async def handle_to_postgis(payload: UploadRequest, user_id: int = 2): try: + table_name = await generate_unique_table_name(payload.title) + df = pd.DataFrame(payload.rows) - print(f"[INFO] Diterima {len(df)} baris data dari frontend.") + df.columns = [col.upper() for col in df.columns] - # --- Validasi kolom geometry --- - if "geometry" not in df.columns: - raise errorRes(status_code=400, message="Kolom 'geometry' tidak ditemukan dalam data.") + if "GEOMETRY" not in df.columns: + raise HTTPException(400, "Kolom GEOMETRY tidak ditemukan") - # --- Parsing geometry ke objek shapely --- - df["geometry"] = df["geometry"].apply( + df["GEOMETRY"] = df["GEOMETRY"].apply( lambda g: wkt.loads(g) - if isinstance(g, str) and g.strip().upper().startswith(VALID_WKT_PREFIXES) - else None + if isinstance(g, str) else None ) - # --- Buat GeoDataFrame --- - gdf = gpd.GeoDataFrame(df, geometry="geometry", crs="EPSG:4326") + gdf = gpd.GeoDataFrame(df, geometry="GEOMETRY", crs="EPSG:4326") - # --- Metadata info dari payload --- - # dataset_title = getattr(payload, "dataset_title", None) - # dataset_year = getattr(payload, "dataset_year", None) - # dataset_desc = getattr(payload, "dataset_description", None) - dataset_title = "longsor 2020" - dataset_year = 2020 - dataset_desc = "test metadata" + # --- Wajib: gunakan engine sync TANPA asyncpg + loop = asyncio.get_running_loop() + await loop.run_in_executor( + None, + lambda: gdf.to_postgis( + table_name, + sync_engine, # JANGAN ENGINE ASYNC + if_exists="replace", + index=False + ) + ) - if not dataset_title: - raise errorRes(status_code=400, detail="Field 'dataset_title' wajib ada untuk metadata.") + # === STEP 4: add ID column === + async with engine.begin() as conn: + await conn.execute(text( + f'ALTER TABLE "{table_name}" ADD COLUMN _ID SERIAL PRIMARY KEY;' + )) + + # === STEP 5: save author metadata === + author = payload.author async with engine.begin() as conn: - fields = [col for col in df.columns if col != "geometry"] - # 💾 1️⃣ Simpan Metadata Dataset - print("[INFO] Menyimpan metadata dataset...") - result = await conn.execute( - text(""" - INSERT INTO dataset_metadata (user_id, title, year, description, fields, created_at) - VALUES (:user_id, :title, :year, :desc, :fields, :created_at) - RETURNING id; - """), - { - "user_id": user_id, - "title": dataset_title, - "year": dataset_year, - "desc": dataset_desc, - "fields": json.dumps(fields), - "created_at": datetime.utcnow(), - }, - ) - metadata_id = result.scalar_one() - print(f"[INFO] Metadata disimpan dengan ID {metadata_id}") - - # ⚙️ 2️⃣ Auto-create Partisi Jika Belum Ada - print(f"[INFO] Memastikan partisi test_partition_user_{user_id} tersedia...") - await conn.execute( - text(f""" - DO $$ - BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_tables WHERE tablename = 'test_partition_user_{user_id}' - ) THEN - EXECUTE format(' - CREATE TABLE test_partition_user_%s - PARTITION OF test_partition - FOR VALUES IN (%s); - ', {user_id}, {user_id}); - EXECUTE format('CREATE INDEX IF NOT EXISTS idx_partition_user_%s_geom ON test_partition_user_%s USING GIST (geom);', {user_id}, {user_id}); - EXECUTE format('CREATE INDEX IF NOT EXISTS idx_partition_user_%s_metadata ON test_partition_user_%s (metadata_id);', {user_id}, {user_id}); - END IF; - END - $$; - """) - ) - - # 🧩 3️⃣ Insert Data Spasial ke Partisi - print(f"[INFO] Memasukkan data ke test_partition_user_{user_id} ...") - insert_count = 0 - for _, row in gdf.iterrows(): - geom_wkt = row["geometry"].wkt if row["geometry"] is not None else None - attributes = row.drop(labels=["geometry"]).to_dict() - - await conn.execute( - text(""" - INSERT INTO test_partition (user_id, metadata_id, geom, attributes, created_at) - VALUES (:user_id, :metadata_id, ST_Force2D(ST_GeomFromText(:geom, 4326)), - CAST(:attr AS jsonb), :created_at); - """), - { - "user_id": user_id, - "metadata_id": metadata_id, - "geom": geom_wkt, - "attr": json.dumps(attributes), - "created_at": datetime.utcnow(), - }, + await conn.execute(text(""" + INSERT INTO backend.author_metadata ( + table_title, + dataset_title, + dataset_abstract, + keywords, + topic_category, + date_created, + dataset_status, + organization_name, + contact_person_name, + contact_email, + contact_phone, + geom_type, + user_id + ) VALUES ( + :table_title, + :dataset_title, + :dataset_abstract, + :keywords, + :topic_category, + :date_created, + :dataset_status, + :organization_name, + :contact_person_name, + :contact_email, + :contact_phone, + :geom_type, + :user_id ) - insert_count += 1 + """), { + "table_title": table_name, + "dataset_title": author.get("title") or payload.title, + "dataset_abstract": author.get("abstract"), + "keywords": author.get("keywords"), + "topic_category": author.get("topicCategory"), + "date_created": str_to_date(author.get("dateCreated")), + "dataset_status": author.get("status"), + "organization_name": author.get("organization"), + "contact_person_name": author.get("contactName"), + "contact_email": author.get("contactEmail"), + "contact_phone": author.get("contactPhone"), + "geom_type": json.dumps(list(gdf.geom_type.unique())), + "user_id": user_id + }) - # 🧩 4️⃣ Membuat VIEW untuk dataset baru di QGIS - await create_dataset_view_from_metadata(conn, metadata_id, user_id, dataset_title) + # === STEP 6: log success === + await log_activity( + user_id=user_id, + action_type="UPLOAD", + action_title=f"Upload dataset {table_name}", + details={"table_name": table_name, "rows": len(gdf)} + ) - print(f"[INFO] ✅ Berhasil memasukkan {insert_count} baris ke partisi user_id={user_id} (metadata_id={metadata_id}).") - - return { + res = { + "table_name": table_name, "status": "success", - "user_id": user_id, - "metadata_id": metadata_id, - "dataset_title": dataset_title, - "inserted_rows": insert_count, + "message": f"Tabel '{table_name}' berhasil dibuat.", + "total_rows": len(gdf), "geometry_type": list(gdf.geom_type.unique()), } + + return successRes(data=res) except Exception as e: - print(f"[ERROR] Gagal upload ke PostGIS partition: {e}") - raise errorRes(status_code=500, message="Gagal upload ke PostGIS partition", details=str(e)) \ No newline at end of file + await log_activity( + user_id=user_id, + action_type="ERROR", + action_title="Upload gagal", + details={"error": str(e)} + ) + print(f"error : {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + + + + + + + + + + + + + + diff --git a/services/upload_file/utils/__pycache__/geometry_detector.cpython-39.pyc b/services/upload_file/utils/__pycache__/geometry_detector.cpython-39.pyc deleted file mode 100644 index 5a212a8cb9ee38d12010c6679665ea253576fc04..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13831 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 diff --git a/utils/logger_config.py b/utils/logger_config.py index 4d2803a..55f1367 100644 --- a/utils/logger_config.py +++ b/utils/logger_config.py @@ -1,5 +1,8 @@ import logging import os +import json +from sqlalchemy import text +from database.connection import engine LOG_DIR = "logs" os.makedirs(LOG_DIR, exist_ok=True) @@ -30,3 +33,33 @@ def setup_logger(name: str): logger.addHandler(console_handler) return logger + + + + + +async def log_activity(user_id, action_type, action_title, details=None): + payload = { + "user_id": user_id, + "action_type": action_type, + "action_title": action_title, + "action_details": json.dumps(details) if details else None + } + + async with engine.begin() as conn: + await conn.execute( + text(""" + INSERT INTO backend.system_logs ( + user_id, + action_type, + action_title, + action_details + ) VALUES ( + :user_id, + :action_type, + :action_title, + :action_details + ) + """), + payload + )