Compare commits
10
Commits
b63ca268b9
...
d1f6c1830f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1f6c1830f | ||
|
|
c7b1929fdc | ||
|
|
189df529d9 | ||
|
|
abb1c01753 | ||
|
|
7e25cd6674 | ||
|
|
db0cc4007c | ||
|
|
3061b86524 | ||
|
|
cc7cd9876c | ||
|
|
f7426fb6ea | ||
|
|
eae9248dc5 |
@@ -0,0 +1,27 @@
|
||||
"""
|
||||
مدل ORM برای جدول const در MySQL.
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String, Text
|
||||
from core.service.mysql_orm import Base
|
||||
|
||||
|
||||
class Const(Base):
|
||||
"""مدل جدول const"""
|
||||
__tablename__ = "const"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
key = Column(String(255), nullable=False, index=True)
|
||||
title_fa = Column(String(255), nullable=True)
|
||||
value = Column(Text, nullable=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Const(id={self.id}, key='{self.key}', title_fa='{self.title_fa}')>"
|
||||
|
||||
def to_dict(self):
|
||||
"""تبدیل به dict برای JSON response"""
|
||||
return {
|
||||
"id": self.id,
|
||||
"key": self.key,
|
||||
"title_fa": self.title_fa,
|
||||
"value": self.value,
|
||||
}
|
||||
@@ -48,3 +48,5 @@ class RegisterStatusModel(BaseModel):
|
||||
amount: Optional[AmountStatus] = Field(description="مبالغ")
|
||||
related_to_number_start:Optional[int] = Field(description="شماره صورت وضعیت مرتبط")
|
||||
related_to_number_end:Optional[int] = Field(description="شماره صورت وضعیت مرتبط")
|
||||
date_at: Optional[datetime.datetime] = Field(description='تاریخ پرداخت')
|
||||
amount_ّfinancial: Optional[int] = Field(description='مبلغ صورت وضعیت در قسمت مالی')
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
"""
|
||||
Repository برای جدول const در MySQL - برای استفاده در خروجی اکسل.
|
||||
"""
|
||||
from typing import List, Dict, Optional
|
||||
from sqlalchemy import select
|
||||
from core.service.mysql_orm import get_mysql_session
|
||||
from app.models.mysql_const import Const
|
||||
|
||||
|
||||
class MySQLConstRepository:
|
||||
"""Repository برای خواندن دیتا از جدول const"""
|
||||
|
||||
async def get_by_keys(self, keys: List[str]) -> Dict[int, str]:
|
||||
"""
|
||||
گرفتن const ها با لیست key ها و برگرداندن dict به صورت {id: title_fa}
|
||||
مثال: get_by_keys(['nature', 'type']) -> {2: 'طبیعت 2', 9: 'نوع 9'}
|
||||
"""
|
||||
async with get_mysql_session() as session:
|
||||
result = await session.execute(
|
||||
select(Const).where(Const.key.in_(keys))
|
||||
)
|
||||
consts = result.scalars().all()
|
||||
return {c.id: c.title_fa or c.value or str(c.id) for c in consts}
|
||||
|
||||
async def get_by_key(self, key: str) -> Dict[int, str]:
|
||||
"""گرفتن const ها با یک key و برگرداندن dict به صورت {id: title_fa}"""
|
||||
return await self.get_by_keys([key])
|
||||
|
||||
async def get_all_consts_dict(self) -> Dict[int, str]:
|
||||
"""گرفتن همه const ها به صورت {id: title_fa} - برای کش کردن"""
|
||||
async with get_mysql_session() as session:
|
||||
result = await session.execute(select(Const))
|
||||
consts = result.scalars().all()
|
||||
return {c.id: c.title_fa or c.value or str(c.id) for c in consts}
|
||||
@@ -261,8 +261,10 @@ class ProjectRepository(BaseRepository[ProjectModel]):
|
||||
_set = ProjectModel(
|
||||
sts=1,
|
||||
counter=i['id'],
|
||||
design=Design(**i)
|
||||
).init_id().cu_at()
|
||||
design=Design(**i),
|
||||
created_at=i['created_at'],
|
||||
updated_at=i['updated_at'],
|
||||
).init_id()
|
||||
obj.append(_set.mongo_dict())
|
||||
|
||||
await use_collection(TaskEnv.CLN_PROJECT).insert_many(obj)
|
||||
|
||||
@@ -19,3 +19,57 @@ class RegisterStatusRepository(BaseRepository):
|
||||
async def get_list(self,project_id:uuid.UUID):
|
||||
list_status = await use_collection(TaskEnv.CLN_AUDIT_STATUS).find({"project_id":project_id}).to_list(None)
|
||||
return list_status
|
||||
async def create(self, coding: RegisterStatusModel):
|
||||
result = await super().create(coding)
|
||||
price = await use_collection(TaskEnv.CLN_PROJECT_PRICE_REPORT).find_one({'project_id': coding.project_id})
|
||||
if price and coding.amount_ّfinancial:
|
||||
if coding.sts ==1:
|
||||
await use_collection(TaskEnv.CLN_PROJECT_PRICE_REPORT).update_one(
|
||||
{'project_id': coding.project_id},
|
||||
{"$inc": {'amount_status': coding.amount_ّfinancial}}
|
||||
)
|
||||
elif coding.sts == 2:
|
||||
await use_collection(TaskEnv.CLN_PROJECT_PRICE_REPORT).update_one(
|
||||
{'project_id': coding.project_id},
|
||||
{"$inc": {'amount_adjust': coding.amount_ّfinancial}}
|
||||
)
|
||||
return result
|
||||
|
||||
async def update(self, _id: uuid.UUID, _update: RegisterStatusModel):
|
||||
_old_dic = await use_collection(TaskEnv.CLN_AUDIT_STATUS).find_one({'_id':_id})
|
||||
change = 0
|
||||
result = await super().update(_id, _update)
|
||||
price = await use_collection(TaskEnv.CLN_PROJECT_PRICE_REPORT).find_one({'project_id': _update.project_id})
|
||||
if price:
|
||||
_old = RegisterStatusModel(**_old_dic)
|
||||
if 'amount_ّfinancial' not in _old_dic and _update.amount_ّfinancial:
|
||||
change = _update.amount_ّfinancial
|
||||
if _old.amount_ّfinancial != _update.amount_ّfinancial:
|
||||
change = _update.amount_ّfinancial - _old.amount_ّfinancial
|
||||
if change != 0 :
|
||||
if _old.sts == 1:
|
||||
await use_collection(TaskEnv.CLN_PROJECT_PRICE_REPORT).update_one(
|
||||
{'project_id': _update.project_id},
|
||||
{"$inc": {'amount_status': change}}
|
||||
)
|
||||
elif _old.sts == 2:
|
||||
await use_collection(TaskEnv.CLN_PROJECT_PRICE_REPORT).update_one(
|
||||
{'project_id': _update.project_id},
|
||||
{"$inc": {'amount_adjust': change}}
|
||||
)
|
||||
return result
|
||||
async def delete(self, _id: uuid.UUID):
|
||||
_old_dic = await use_collection(TaskEnv.CLN_AUDIT_STATUS).find_one({'_id': _id})
|
||||
await super().delete(_id)
|
||||
price = await use_collection(TaskEnv.CLN_PROJECT_PRICE_REPORT).find_one({'project_id': _old_dic['project_id']})
|
||||
if price and _old_dic['sts'] == 1 and 'amount_status' in price:
|
||||
await use_collection(TaskEnv.CLN_PROJECT_PRICE_REPORT).update_one(
|
||||
{'project_id': _old_dic['project_id']},
|
||||
{"$inc": {'amount_status': -(_old_dic['amount_ّfinancial'])}}
|
||||
)
|
||||
if price and _old_dic['sts'] == 2 and 'amount_adjust' in price:
|
||||
await use_collection(TaskEnv.CLN_PROJECT_PRICE_REPORT).update_one(
|
||||
{'project_id': _old_dic['project_id']},
|
||||
{"$inc": {'amount_adjust': -(_old_dic['amount_ّfinancial'])}}
|
||||
)
|
||||
return True
|
||||
|
||||
@@ -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 *
|
||||
@@ -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)
|
||||
+2
-2
@@ -5,8 +5,8 @@ from core.service.mongo_db import use_collection
|
||||
|
||||
|
||||
def get_auth_db():
|
||||
return conf.server.is_prod and conf.database.get('mongo').production.db or conf.database.get(
|
||||
'mongo').dev.db
|
||||
return conf.server.is_prod and conf.database.get('mongo_auth').production.db or conf.database.get(
|
||||
'mongo_auth').dev.db
|
||||
|
||||
|
||||
def use_auth_collection(collection: str) -> Collection:
|
||||
|
||||
+15
@@ -81,3 +81,18 @@ database:
|
||||
version: 4
|
||||
password: 'sample'
|
||||
user: 'local'
|
||||
|
||||
# MySQL (read-only) - جدا از database تا مونگو و بقیه بدون تغییر بمانند
|
||||
mysql:
|
||||
dev:
|
||||
host: '172.16.1.194'
|
||||
port: 3309
|
||||
user: 'root'
|
||||
password: '2TjwtsnYAs'
|
||||
db: 'doc'
|
||||
production:
|
||||
host: '172.16.1.194'
|
||||
port: 3309
|
||||
user: 'root'
|
||||
password: '2TjwtsnYAs'
|
||||
db: 'doc'
|
||||
|
||||
+38
-4
@@ -1,10 +1,10 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Union
|
||||
from typing import Optional, List, Union, Dict, Any
|
||||
|
||||
import pydantic
|
||||
import yaml
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, validator
|
||||
|
||||
|
||||
def build_path(path: Union[Path, str], path_is_absolute: bool = False) -> Path:
|
||||
@@ -73,6 +73,19 @@ class DatabaseModeConfig(BaseModel):
|
||||
production: Optional[DatabaseConfig]
|
||||
|
||||
|
||||
class MySQLConfig(BaseModel):
|
||||
host: str
|
||||
port: int
|
||||
user: str
|
||||
password: str
|
||||
db: str
|
||||
|
||||
|
||||
class MySQLModeConfig(BaseModel):
|
||||
dev: Optional[MySQLConfig]
|
||||
production: Optional[MySQLConfig]
|
||||
|
||||
|
||||
class DependencyConfig(BaseModel):
|
||||
middleware: Optional[List[str]]
|
||||
routes: Optional[List[str]]
|
||||
@@ -115,12 +128,33 @@ class CoreConfig(pydantic.BaseModel):
|
||||
server: Optional[ServerConfig]
|
||||
auth: Optional[AuthConfig]
|
||||
dependency: Optional[DependencyConfig]
|
||||
database: dict[str, Union[DatabaseConfig, DatabaseModeConfig]]
|
||||
database: Dict[str, Union[DatabaseConfig, DatabaseModeConfig]]
|
||||
mysql: Optional[MySQLModeConfig] = None
|
||||
static_files: dict[str, str] = dict()
|
||||
celery: Optional[CeleryTask]
|
||||
|
||||
@validator('database', pre=True)
|
||||
def parse_database(cls, v: Any) -> Dict[str, Union[DatabaseConfig, DatabaseModeConfig]]:
|
||||
if not isinstance(v, dict):
|
||||
return v
|
||||
result: Dict[str, Union[DatabaseConfig, DatabaseModeConfig]] = {}
|
||||
for key, val in v.items():
|
||||
if not isinstance(val, dict):
|
||||
result[key] = val
|
||||
continue
|
||||
if 'dev' in val or 'production' in val:
|
||||
dev = val.get('dev')
|
||||
prod = val.get('production')
|
||||
result[key] = DatabaseModeConfig(
|
||||
dev=DatabaseConfig(**(dev or {})) if dev else None,
|
||||
production=DatabaseConfig(**(prod or {})) if prod else None,
|
||||
)
|
||||
else:
|
||||
result[key] = DatabaseConfig(**val)
|
||||
return result
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = False
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
# def load_modules_config(self):
|
||||
# # import dynamic routes in the modules
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
اتصال MySQL فقط-خواندنی (read-only).
|
||||
از پول کانکشن استفاده میکند و در هر کانکشن SESSION را به حالت فقط-خواندنی تنظیم میکند.
|
||||
"""
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Optional
|
||||
|
||||
import aiomysql
|
||||
|
||||
from core.utils.mysql import get_mysql_config
|
||||
|
||||
_pool: Optional[aiomysql.Pool] = None
|
||||
_pool_connection_name: Optional[str] = None
|
||||
|
||||
|
||||
async def get_mysql_pool(connection_name: str = None) -> aiomysql.Pool:
|
||||
"""پول کانکشن MySQL را برمیگرداند؛ در اولین فراخوانی ساخته میشود."""
|
||||
global _pool, _pool_connection_name
|
||||
name = connection_name or "mysql"
|
||||
if _pool is None or _pool_connection_name != name:
|
||||
if _pool is not None:
|
||||
_pool.close()
|
||||
await _pool.wait_closed()
|
||||
cfg = get_mysql_config(name)
|
||||
_pool = await aiomysql.create_pool(
|
||||
host=cfg["host"],
|
||||
port=cfg["port"],
|
||||
user=cfg["user"],
|
||||
password=cfg["password"],
|
||||
db=cfg["db"],
|
||||
charset="utf8mb4",
|
||||
autocommit=True,
|
||||
minsize=1,
|
||||
maxsize=10,
|
||||
)
|
||||
_pool_connection_name = name
|
||||
return _pool
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def get_mysql_connection(connection_name: str = None):
|
||||
"""
|
||||
کانکشن فقط-خواندنی از پول میگیرد.
|
||||
در ابتدا SESSION را به حالت فقط-خواندنی تنظیم میکند (برای اطمینان بیشتر).
|
||||
استفاده:
|
||||
async with get_mysql_connection() as conn:
|
||||
async with conn.cursor(aiomysql.DictCursor) as cur:
|
||||
await cur.execute("SELECT * FROM t LIMIT 10")
|
||||
rows = await cur.fetchall()
|
||||
"""
|
||||
pool = await get_mysql_pool(connection_name)
|
||||
conn = await pool.acquire()
|
||||
try:
|
||||
async with conn.cursor() as cur:
|
||||
await cur.execute("SET SESSION TRANSACTION READ ONLY")
|
||||
yield conn
|
||||
finally:
|
||||
pool.release(conn)
|
||||
|
||||
|
||||
async def close_mysql_pool():
|
||||
"""بستن پول (مثلاً در shutdown اپ)."""
|
||||
global _pool
|
||||
if _pool is not None:
|
||||
_pool.close()
|
||||
await _pool.wait_closed()
|
||||
_pool = None
|
||||
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
SQLAlchemy async setup برای MySQL (فقط خواندنی).
|
||||
از asyncmy به عنوان async driver استفاده میکند.
|
||||
"""
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import declarative_base, sessionmaker
|
||||
|
||||
from core.utils.mysql import get_mysql_config
|
||||
|
||||
# Base class برای تمام مدلهای ORM
|
||||
Base = declarative_base()
|
||||
|
||||
_engine = None
|
||||
_async_session_maker = None
|
||||
|
||||
|
||||
def get_mysql_url(connection_name: str = None) -> str:
|
||||
"""ساخت URL برای SQLAlchemy از تنظیمات MySQL"""
|
||||
cfg = get_mysql_config(connection_name)
|
||||
return f"mysql+asyncmy://{cfg['user']}:{cfg['password']}@{cfg['host']}:{cfg['port']}/{cfg['db']}?charset=utf8mb4"
|
||||
|
||||
|
||||
def get_async_engine(connection_name: str = None):
|
||||
"""برگرداندن async engine (در اولین فراخوانی ساخته میشود)"""
|
||||
global _engine
|
||||
if _engine is None:
|
||||
url = get_mysql_url(connection_name)
|
||||
_engine = create_async_engine(
|
||||
url,
|
||||
echo=False, # برای debug میتونی True کنی
|
||||
pool_pre_ping=True, # چک کردن کانکشن قبل از استفاده
|
||||
pool_size=5,
|
||||
max_overflow=10,
|
||||
)
|
||||
return _engine
|
||||
|
||||
|
||||
def get_async_session_maker(connection_name: str = None):
|
||||
"""برگرداندن async sessionmaker"""
|
||||
global _async_session_maker
|
||||
if _async_session_maker is None:
|
||||
engine = get_async_engine(connection_name)
|
||||
_async_session_maker = sessionmaker(
|
||||
engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False,
|
||||
autocommit=False,
|
||||
autoflush=False,
|
||||
)
|
||||
return _async_session_maker
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def get_mysql_session(connection_name: str = None):
|
||||
"""
|
||||
Context manager برای گرفتن async session (فقط خواندنی).
|
||||
استفاده:
|
||||
async with get_mysql_session() as session:
|
||||
result = await session.execute(select(User).where(User.id == 1))
|
||||
user = result.scalar_one_or_none()
|
||||
"""
|
||||
session_maker = get_async_session_maker(connection_name)
|
||||
async with session_maker() as session:
|
||||
# تنظیم session به حالت فقط-خواندنی
|
||||
await session.execute("SET SESSION TRANSACTION READ ONLY")
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
await session.rollback() # برای read-only همیشه rollback میکنیم
|
||||
|
||||
|
||||
async def close_mysql_engine():
|
||||
"""بستن engine (مثلاً در shutdown اپ)."""
|
||||
global _engine, _async_session_maker
|
||||
if _engine is not None:
|
||||
await _engine.dispose()
|
||||
_engine = None
|
||||
_async_session_maker = None
|
||||
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
تنظیمات اتصال MySQL (فقط خواندنی).
|
||||
از بخش جداگانه mysql در config.yml (همسطح با database) استفاده میکند.
|
||||
"""
|
||||
from config import conf
|
||||
|
||||
|
||||
def get_mysql_config(connection_name: str = None):
|
||||
"""
|
||||
بر اساس محیط (dev/production) تنظیمات MySQL را برمیگرداند.
|
||||
Returns: dict با کلیدهای host, port, user, password, db
|
||||
"""
|
||||
if not conf.mysql:
|
||||
raise ValueError("تنظیمات mysql در config.yml یافت نشد.")
|
||||
env = "production" if conf.server.is_prod else "dev"
|
||||
mode = getattr(conf.mysql, env, None) or getattr(conf.mysql, "dev", None)
|
||||
if not mode:
|
||||
raise ValueError(f"تنظیمات mysql.{env} در config.yml یافت نشد.")
|
||||
return {
|
||||
"host": getattr(mode, "host", "127.0.0.1"),
|
||||
"port": int(getattr(mode, "port", 3306)),
|
||||
"user": getattr(mode, "user", ""),
|
||||
"password": getattr(mode, "password", ""),
|
||||
"db": getattr(mode, "db", ""),
|
||||
}
|
||||
@@ -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]):
|
||||
|
||||
@@ -62,6 +62,9 @@ class NewTableEngine:
|
||||
if self.return_query:
|
||||
query = await self.exe()
|
||||
return query
|
||||
if self.input.e and self.input.e.ot:
|
||||
query = await self.exe()
|
||||
return query
|
||||
rows, count = await self.exe()
|
||||
self.input.extra_filter = None
|
||||
del self.engine
|
||||
|
||||
+186
-16
@@ -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
|
||||
@@ -9,6 +16,8 @@ from celery import shared_task
|
||||
|
||||
from .AbstractEngine import AbstractEngineClass
|
||||
from ..utils.Models import FilterItem, FilterScope, OutPutType
|
||||
from ..utils.excel_value_maps import EXCEL_VALUE_MAP_REGISTRY
|
||||
from app.repositories.mysql_const_repo import MySQLConstRepository
|
||||
|
||||
|
||||
# def zero_time_data(date, is_max=False):
|
||||
@@ -31,25 +40,26 @@ class MongoEngine(AbstractEngineClass):
|
||||
rows = []
|
||||
stack = []
|
||||
filters = {}
|
||||
_const_cache: dict = None # کش const ها از MySQL
|
||||
|
||||
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)
|
||||
else:
|
||||
tmp = tmp.sort(sort_by)
|
||||
|
||||
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)
|
||||
res = await tmp.to_list(None)
|
||||
self.input.e.d = await self.generate_output(res)
|
||||
return {'query': _f, 'sort': self.set_sort(), 'path': self.input.e.d}
|
||||
|
||||
self.rows = tmp.limit(self.input.get_limit()).skip(self.input.skip())
|
||||
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 +259,11 @@ class MongoEngine(AbstractEngineClass):
|
||||
def set_joins(self):
|
||||
pass
|
||||
|
||||
def generate_output(self, query, _f):
|
||||
print(_f.__dict__)
|
||||
return ''
|
||||
async 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 await 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 +271,171 @@ 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 _expand_headers_with_id_fields(self, cols: dict) -> list:
|
||||
"""
|
||||
اگر design.nature در فیلدها باشد، design.nature_id هم اضافه میشود.
|
||||
اگر design.budget در فیلدها باشد، design.budget_id هم اضافه میشود.
|
||||
"""
|
||||
headers = list(cols.keys())
|
||||
expanded = []
|
||||
pair_rules = [
|
||||
('design.nature', 'design.nature_id', 'نام ماهیت'),
|
||||
('design.budget', 'design.budget_id', 'نام دسته بودجه'),
|
||||
]
|
||||
for h in headers:
|
||||
expanded.append(h)
|
||||
for field, id_field, default_title in pair_rules:
|
||||
if h == field and id_field not in headers and id_field not in expanded:
|
||||
expanded.append(id_field)
|
||||
cols[id_field] = cols.get(field, default_title) + ' (نام)'
|
||||
break
|
||||
return expanded
|
||||
|
||||
async 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 = self._expand_headers_with_id_fields(cols)
|
||||
|
||||
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)
|
||||
|
||||
# ساختن value_maps از vk (نام مپ) با استفاده از رجیستری
|
||||
value_maps = self._build_value_maps()
|
||||
|
||||
# بارگذاری const ها برای فیلدهای خاص
|
||||
const_maps = await self._load_const_maps(headers)
|
||||
|
||||
for row_index, item in enumerate(datas, start=1):
|
||||
for col_index, col in enumerate(headers):
|
||||
val = self.get_nested_value(item, col)
|
||||
val = self._apply_value_map(col, val, value_maps)
|
||||
val = await self._apply_const_map(col, val, const_maps)
|
||||
worksheet.write(row_index, col_index, val)
|
||||
workbook.close()
|
||||
return file_name
|
||||
|
||||
def _build_value_maps(self):
|
||||
"""اگر کاربر در e.vk برای ستونی کلید مپ داده (مثلاً design_sts)، مپ از رجیستری برمیگردد."""
|
||||
value_maps = {}
|
||||
vk = getattr(self.input.e, "vk", None) or {}
|
||||
if not isinstance(vk, dict):
|
||||
return value_maps
|
||||
for col_key, map_name in vk.items():
|
||||
if map_name and map_name in EXCEL_VALUE_MAP_REGISTRY:
|
||||
value_maps[col_key] = EXCEL_VALUE_MAP_REGISTRY[map_name]
|
||||
return value_maps
|
||||
|
||||
def _apply_value_map(self, col_key: str, val, value_maps: dict):
|
||||
"""اگر برای این ستون مپ تعریف شده، مقدار را به متن متناظر تبدیل میکند."""
|
||||
if not value_maps or col_key not in value_maps:
|
||||
return val
|
||||
mapping = value_maps[col_key]
|
||||
if not isinstance(mapping, dict):
|
||||
return val
|
||||
if val is None:
|
||||
return mapping.get(None, val)
|
||||
key = val if isinstance(val, (int, float)) else (int(val) if isinstance(val, str) and str(val).strip().isdigit() else val)
|
||||
if key in mapping:
|
||||
return mapping[key]
|
||||
if isinstance(key, int) and str(key) in mapping:
|
||||
return mapping[str(key)]
|
||||
return val
|
||||
|
||||
async def _load_const_maps(self, headers: List[str]) -> dict:
|
||||
"""
|
||||
شناسایی فیلدهای خاص (مثلاً nature_id, type_id, budget_id) و بارگذاری const ها از MySQL.
|
||||
Returns: dict به صورت {col_key: {id: title_fa}} مثلاً {"design.nature_id": {2: "طبیعت 2", ...}}
|
||||
"""
|
||||
# لیست فیلدهای خاص که باید از const خوانده شوند
|
||||
const_field_names = ['nature_id', 'type_id', 'budget_id'] # میتونی اضافه کنی
|
||||
|
||||
const_maps = {}
|
||||
const_keys_to_load = set()
|
||||
const_column_map = {} # {col_key: const_key} مثلاً {"design.nature_id": "nature"}
|
||||
|
||||
# شناسایی ستونهایی که باید const بخونن
|
||||
for col_key in headers:
|
||||
col_name = col_key.split('.')[-1] # آخرین بخش (nature_id)
|
||||
if col_name in const_field_names:
|
||||
const_key = col_name.replace('_id', '') # nature_id -> nature
|
||||
const_keys_to_load.add(const_key)
|
||||
const_column_map[col_key] = const_key
|
||||
|
||||
# بارگذاری const ها از MySQL
|
||||
if const_keys_to_load:
|
||||
repo = MySQLConstRepository()
|
||||
for col_key, const_key in const_column_map.items():
|
||||
consts_for_key = await repo.get_by_key(const_key)
|
||||
if consts_for_key:
|
||||
const_maps[col_key] = consts_for_key
|
||||
|
||||
return const_maps
|
||||
|
||||
async def _apply_const_map(self, col_key: str, val, const_maps: dict):
|
||||
"""اگر این ستون در const_maps باشد، id را به title_fa تبدیل میکند."""
|
||||
if not const_maps or col_key not in const_maps:
|
||||
return val
|
||||
|
||||
mapping = const_maps[col_key]
|
||||
if not isinstance(mapping, dict):
|
||||
return val
|
||||
|
||||
if val is None:
|
||||
return mapping.get(None, val)
|
||||
|
||||
# تبدیل val به int برای جستجو در mapping
|
||||
key = val if isinstance(val, (int, float)) else (int(val) if isinstance(val, str) and str(val).strip().isdigit() else val)
|
||||
|
||||
if key in mapping:
|
||||
return mapping[key]
|
||||
if isinstance(key, int) and str(key) in mapping:
|
||||
return mapping[str(key)]
|
||||
|
||||
return val
|
||||
|
||||
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):
|
||||
JSON = 'json'
|
||||
EXEL = 'exel'
|
||||
EXEL = 'excel'
|
||||
PDF = 'pdf'
|
||||
|
||||
|
||||
@@ -81,6 +81,7 @@ class ExportModel(BaseModel):
|
||||
ot: Optional[OutPutType]
|
||||
c: Optional[dict]
|
||||
d: Optional[str]
|
||||
vk: Optional[dict] = None
|
||||
|
||||
|
||||
class SortModel(BaseModel):
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
رجیستری مپهای ازپیشتعریفشده برای خروجی اکسل.
|
||||
کلاینت فقط کلید (مثلاً design_sts) را در e.vk میفرستد تا بهجای عدد، متن در اکسل بیاید.
|
||||
"""
|
||||
|
||||
EXCEL_VALUE_MAP_REGISTRY = {
|
||||
"design_sts": {
|
||||
1: "شروع نشده",
|
||||
2: "جاری",
|
||||
3: "متوقف",
|
||||
4: "خاتمه یافته",
|
||||
},
|
||||
"monitor_sts": {
|
||||
1: "شروع نشده",
|
||||
2: "جاری",
|
||||
3: "متوقف",
|
||||
4: "تحویل موقت",
|
||||
5: "خاتمه یافته",
|
||||
},
|
||||
|
||||
"sts": {
|
||||
1: "جاری",
|
||||
2: "حذف شده",
|
||||
},
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
aiofiles==22.1.0
|
||||
aiomysql==0.2.0
|
||||
aioredis==2.0.1
|
||||
asyncmy==0.2.9
|
||||
amqp==5.1.1
|
||||
annotated-types==0.6.0
|
||||
anyio==3.6.2
|
||||
|
||||
Reference in New Issue
Block a user