feat(root):add create export

This commit is contained in:
shahzeydyfah
2026-02-05 23:02:08 +03:30
parent b63ca268b9
commit eae9248dc5
6 changed files with 132 additions and 16 deletions
+2 -1
View File
@@ -18,4 +18,5 @@ from .financial_status import *
from .financial_adjust import *
from .delivery import *
from .report import *
from .excel import *
from .excel import *
from .download import *
+48
View File
@@ -0,0 +1,48 @@
"""
دانلود فایل اکسل تولیدشده از خروجی جدول (Table Engine).
فایل‌ها در پوشه temp پروژه ذخیره می‌شوند و از طریق این endpoint قابل دانلود هستند.
"""
import os
import re
from fastapi import APIRouter, HTTPException
from starlette.responses import FileResponse
from config import conf
from core.app import app
from core.utils.router import generate_router
router = generate_router("download", auth_dependencies=False, version="v2")
_tag_base = ["download"]
_pre = conf.server.prefix_url.replace("v1", "v2")
def _excel_temp_dir():
return os.path.join(os.getcwd(), "temp")
def _safe_excel_filename(name: str) -> bool:
"""فقط نام فایل اکسلی با فرمت uuid.xlsx (۳۲ کاراکتر hex + .xlsx) مجاز است."""
return bool(re.match(r"^[a-f0-9]{32}\.xlsx$", name))
@router.get("/excel/{filename}", tags=_tag_base)
async def download_excel(filename: str):
"""
دانلود فایل اکسل با نام بازگردانده‌شده از API لیست (فیلد path).
مثال: بعد از درخواست خروجی اکسل، در پاسخ path مثلاً «abc123...xlsx» می‌آید؛
برای دانلود: GET /v2/download/excel/abc123....xlsx
"""
if not _safe_excel_filename(filename):
raise HTTPException(status_code=400, detail="نام فایل نامعتبر است.")
file_path = os.path.join(_excel_temp_dir(), filename)
if not os.path.isfile(file_path):
raise HTTPException(status_code=404, detail="فایل یافت نشد یا منقضی شده است.")
return FileResponse(
file_path,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
filename=filename,
)
app.include_router(router)
+1
View File
@@ -10,6 +10,7 @@ class TableEngineInputType(GenericModel, Generic[T]):
o: Optional[dict] = {}
p: Optional[dict] = {}
s: Optional[dict] = {}
e: Optional[dict] = {}
class TableEngineExport(GenericModel, Generic[T]):
+3
View File
@@ -62,6 +62,9 @@ class NewTableEngine:
if self.return_query:
query = await self.exe()
return query
if self.input.e:
query = await self.exe()
return query
rows, count = await self.exe()
self.input.extra_filter = None
del self.engine
+77 -14
View File
@@ -2,6 +2,13 @@ import datetime
import uuid
from typing import Union, List
from uuid import UUID
import os
import xlsxwriter
import tempfile
import string
import copy
import jdatetime
# import dateutil.parser
from celery import shared_task
@@ -34,12 +41,10 @@ class MongoEngine(AbstractEngineClass):
async def exe(self):
_f = self.filters = self.set_filters()
print("_f ====>", _f)
if self.unselect:
tmp = self.query_class.find(_f, self.unselect)
else:
tmp = self.query_class.find(_f)
sort_by = self.set_sort()
if isinstance(sort_by, dict):
tmp = tmp.sort(**sort_by)
@@ -49,7 +54,11 @@ class MongoEngine(AbstractEngineClass):
self.rows = tmp.limit(
self.input.get_limit()).skip(self.input.skip())
if self.input.e and self.input.e.ot:
self.input.e.d = self.generate_output(self.query_class, _f)
copy_list = copy.deepcopy(tmp)
res = await copy_list.to_list(None)
self.input.e.d = self.generate_output(res)
return {'query': _f, 'sort': self.set_sort(), 'path': self.input.e.d}
if self.return_query:
return {'query':_f, 'sort':self.set_sort(), 'limit':self.input.get_limit(), 'page':self.input.skip()}
return [self.model(**document) async for document in self.rows], await self.count()
@@ -249,15 +258,11 @@ class MongoEngine(AbstractEngineClass):
def set_joins(self):
pass
def generate_output(self, query, _f):
print(_f.__dict__)
return ''
def generate_output(self, _f):
print(self.input.e.ot)
if self.input.e.ot == OutPutType.EXEL:
file_name = str(uuid.uuid4().hex) + '.xlsx'
return _f.dict()
MongoEngine.output_generator(str(query).replace('false', '0'), params,
str(self.query_class.bind.url), file_name=file_name, cols=self.input.e.c)
return file_name
return self.output_generator(_f, file_name=file_name, cols=self.input.e.c)
elif self.input.e.ot == OutPutType.JSON:
pass
elif self.input.e.ot == OutPutType.PDF:
@@ -265,7 +270,65 @@ class MongoEngine(AbstractEngineClass):
else:
raise Exception("Output type not supported.")
@staticmethod
@shared_task(name='output_generator_task_mongo')
def output_generator(query, params, url, file_name, cols):
pass
def _excel_temp_dir(self):
"""پوشه temp پروژه برای ذخیره فایل‌های اکسل (همان مسیری که برای دانلود سرو می‌شود)."""
temp_dir = os.path.join(os.getcwd(), 'temp')
os.makedirs(temp_dir, exist_ok=True)
return temp_dir
def output_generator(self,query, file_name, cols):
datas = query
file_dir = os.path.join(self._excel_temp_dir(), file_name)
head_cols = list(str(string.ascii_uppercase))
headers = list(cols.keys())
workbook = xlsxwriter.Workbook(file_dir)
worksheet = workbook.add_worksheet()
# نوشتن هدرها
bold_format = workbook.add_format({'bold': True})
# نوشتن هدرها
for col_index, header in enumerate(headers):
worksheet.write(0, col_index, cols[header], bold_format)
# نوشتن داده‌ها
for row_index, item in enumerate(datas, start=1):
for col_index, col in enumerate(headers):
val = self.get_nested_value(item, col)
worksheet.write(row_index, col_index, val)
workbook.close()
return file_name
def get_nested_value(self, data, path, default='-'):
"""
دریافت مقدار از دیکشنری تو در تو با مسیر dot notation
مثال: path = "design.title"
"""
if not isinstance(path, str):
return default
keys = path.split('.')
val = data
for key in keys:
if isinstance(val, dict):
val = val.get(key, default)
else:
return default
if val is None:
return default
# تبدیل datetime و UUID به رشته برای اکسل
if isinstance(val, datetime.datetime):
return self.create_date(val)
if isinstance(val, uuid.UUID):
return str(val)
return val
def create_date(self, date):
shamsi_dt = jdatetime.datetime.fromgregorian(datetime=date)
return shamsi_dt.strftime("%Y/%m/%d - %H:%M:%S")
+1 -1
View File
@@ -35,7 +35,7 @@ class Cr(str, Enum):
class OutPutType(str, Enum):
JSON = 'json'
EXEL = 'exel'
EXEL = 'excel'
PDF = 'pdf'