feat(root):add create export
This commit is contained in:
@@ -19,3 +19,4 @@ from .financial_adjust import *
|
|||||||
from .delivery import *
|
from .delivery import *
|
||||||
from .report import *
|
from .report import *
|
||||||
from .excel import *
|
from .excel import *
|
||||||
|
from .download import *
|
||||||
@@ -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)
|
||||||
@@ -10,6 +10,7 @@ class TableEngineInputType(GenericModel, Generic[T]):
|
|||||||
o: Optional[dict] = {}
|
o: Optional[dict] = {}
|
||||||
p: Optional[dict] = {}
|
p: Optional[dict] = {}
|
||||||
s: Optional[dict] = {}
|
s: Optional[dict] = {}
|
||||||
|
e: Optional[dict] = {}
|
||||||
|
|
||||||
|
|
||||||
class TableEngineExport(GenericModel, Generic[T]):
|
class TableEngineExport(GenericModel, Generic[T]):
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ class NewTableEngine:
|
|||||||
if self.return_query:
|
if self.return_query:
|
||||||
query = await self.exe()
|
query = await self.exe()
|
||||||
return query
|
return query
|
||||||
|
if self.input.e:
|
||||||
|
query = await self.exe()
|
||||||
|
return query
|
||||||
rows, count = await self.exe()
|
rows, count = await self.exe()
|
||||||
self.input.extra_filter = None
|
self.input.extra_filter = None
|
||||||
del self.engine
|
del self.engine
|
||||||
|
|||||||
@@ -2,6 +2,13 @@ import datetime
|
|||||||
import uuid
|
import uuid
|
||||||
from typing import Union, List
|
from typing import Union, List
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
import os
|
||||||
|
import xlsxwriter
|
||||||
|
import tempfile
|
||||||
|
import string
|
||||||
|
import copy
|
||||||
|
import jdatetime
|
||||||
|
|
||||||
|
|
||||||
# import dateutil.parser
|
# import dateutil.parser
|
||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
@@ -34,12 +41,10 @@ class MongoEngine(AbstractEngineClass):
|
|||||||
|
|
||||||
async def exe(self):
|
async def exe(self):
|
||||||
_f = self.filters = self.set_filters()
|
_f = self.filters = self.set_filters()
|
||||||
print("_f ====>", _f)
|
|
||||||
if self.unselect:
|
if self.unselect:
|
||||||
tmp = self.query_class.find(_f, self.unselect)
|
tmp = self.query_class.find(_f, self.unselect)
|
||||||
else:
|
else:
|
||||||
tmp = self.query_class.find(_f)
|
tmp = self.query_class.find(_f)
|
||||||
|
|
||||||
sort_by = self.set_sort()
|
sort_by = self.set_sort()
|
||||||
if isinstance(sort_by, dict):
|
if isinstance(sort_by, dict):
|
||||||
tmp = tmp.sort(**sort_by)
|
tmp = tmp.sort(**sort_by)
|
||||||
@@ -49,7 +54,11 @@ class MongoEngine(AbstractEngineClass):
|
|||||||
self.rows = tmp.limit(
|
self.rows = tmp.limit(
|
||||||
self.input.get_limit()).skip(self.input.skip())
|
self.input.get_limit()).skip(self.input.skip())
|
||||||
if self.input.e and self.input.e.ot:
|
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:
|
if self.return_query:
|
||||||
return {'query':_f, 'sort':self.set_sort(), 'limit':self.input.get_limit(), 'page':self.input.skip()}
|
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()
|
return [self.model(**document) async for document in self.rows], await self.count()
|
||||||
@@ -249,15 +258,11 @@ class MongoEngine(AbstractEngineClass):
|
|||||||
def set_joins(self):
|
def set_joins(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def generate_output(self, query, _f):
|
def generate_output(self, _f):
|
||||||
print(_f.__dict__)
|
print(self.input.e.ot)
|
||||||
return ''
|
|
||||||
if self.input.e.ot == OutPutType.EXEL:
|
if self.input.e.ot == OutPutType.EXEL:
|
||||||
file_name = str(uuid.uuid4().hex) + '.xlsx'
|
file_name = str(uuid.uuid4().hex) + '.xlsx'
|
||||||
return _f.dict()
|
return self.output_generator(_f, file_name=file_name, cols=self.input.e.c)
|
||||||
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
|
|
||||||
elif self.input.e.ot == OutPutType.JSON:
|
elif self.input.e.ot == OutPutType.JSON:
|
||||||
pass
|
pass
|
||||||
elif self.input.e.ot == OutPutType.PDF:
|
elif self.input.e.ot == OutPutType.PDF:
|
||||||
@@ -265,7 +270,65 @@ class MongoEngine(AbstractEngineClass):
|
|||||||
else:
|
else:
|
||||||
raise Exception("Output type not supported.")
|
raise Exception("Output type not supported.")
|
||||||
|
|
||||||
@staticmethod
|
def _excel_temp_dir(self):
|
||||||
@shared_task(name='output_generator_task_mongo')
|
"""پوشه temp پروژه برای ذخیره فایلهای اکسل (همان مسیری که برای دانلود سرو میشود)."""
|
||||||
def output_generator(query, params, url, file_name, cols):
|
temp_dir = os.path.join(os.getcwd(), 'temp')
|
||||||
pass
|
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")
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class Cr(str, Enum):
|
|||||||
|
|
||||||
class OutPutType(str, Enum):
|
class OutPutType(str, Enum):
|
||||||
JSON = 'json'
|
JSON = 'json'
|
||||||
EXEL = 'exel'
|
EXEL = 'excel'
|
||||||
PDF = 'pdf'
|
PDF = 'pdf'
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user