1181 lines
39 KiB
Python
1181 lines
39 KiB
Python
import os
|
|
import uuid
|
|
import json
|
|
import re
|
|
import datetime
|
|
import hashlib
|
|
import logging
|
|
import threading
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
import django
|
|
from django.conf import settings
|
|
from django.http import HttpResponse, JsonResponse
|
|
|
|
from gvsds.apps.core.models import User, UserAvatar, EphemeralToken, User2FA, UserPending2FA, AccountActivation, UserLoginLog
|
|
from gvsds.apps.gerp.models import (
|
|
Tenant, CompanyInfo, Company, Department, Role, UserRole, RoleDept,
|
|
Permission, FileAsset, WorkOrder, Message, Menu,
|
|
UserABAC, DepartmentSMTM, Password, ItemReference, Backpack,
|
|
SimulatedTrigger, CashReceiptsPaymentsLedger, Asset, Liability,
|
|
)
|
|
|
|
GERPUser = User
|
|
GERPUser2FA = User2FA
|
|
GERPUserPending2FA = UserPending2FA
|
|
GERPRole = Role
|
|
GERPMenu = Menu
|
|
GERPMessage = Message
|
|
GERPCompany = Company
|
|
GERPDept = Department
|
|
GERPPermission = Permission
|
|
GERPUserRole = UserRole
|
|
GERPWorkOrder = WorkOrder
|
|
GERPFileAsset = FileAsset
|
|
GERPTenant = Tenant
|
|
GERPCompanyInfo = CompanyInfo
|
|
GERPCashReceiptsPaymentsLedger = CashReceiptsPaymentsLedger
|
|
GERPAsset = Asset
|
|
GERPLiability = Liability
|
|
GERPBackpack = Backpack
|
|
GERPItemReference = ItemReference
|
|
GERPWorkgroup = type('GERPWorkgroup', (), {})
|
|
from gvsds.model_base import snake_to_pascal
|
|
|
|
|
|
def _pascal_to_snake(name):
|
|
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
|
|
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
|
|
from gvsds.template_engine import ProcessTemplate
|
|
|
|
|
|
class _FlaskCompat:
|
|
request = None
|
|
session = None
|
|
g = type('g', (), {})()
|
|
jsonify = staticmethod(lambda *args, **kwargs: kwargs)
|
|
|
|
@staticmethod
|
|
def Response(content, mimetype='text/html', status=200, headers=None):
|
|
content_type = mimetype or 'text/html'
|
|
resp = HttpResponse(content, content_type=content_type, status=status)
|
|
if headers:
|
|
for k, v in headers.items():
|
|
resp[k] = v
|
|
return resp
|
|
|
|
|
|
flask = _FlaskCompat()
|
|
|
|
UserDefaultAvatar = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\xc0\x00\x00\x00\xc0\x08\x03\x00\x00\x00e\x02\x9c5\x00\x00\x003PLTE\xfa\xfa\xfa\xfa\xfa\xfa\xfa\xfa\xfa\xf0\xf0\xf0\xe0\xe0\xe0\xc8\xc8\xc8\xb8\xb8\xb8\xc0\xc0\xc0\x8f\x8f\x8f\x90\x90\x90\x98\x98\x98\xa0\xa0\xa0\xa8\xa8\xa8\xb0\xb0\xb0\xd0\xd0\xd0\xd8\xd8\xd8\xe8\xe8\xe8\x92\x11\xe9\x17\x00\x00\x00\x02tRNS\xfe\xfd?\xa0\x8f\xaa\x00\x00\x02\x97IDATx\x01\xed\xda\x07\x92\xad:\x0c\x84\xe1y\xc8\xa1G2\xe6\xec\x7f\xb5\xef\xe6\x9c\xa0N3rU\xff+\xe0#\x83\xf5\xb2\xad\xdd\x8b\x00\x02\x08 \x80\x00\x02\x08 \x80\x00\x02\x08 @\x8e\x04\x10@\x00\x01\x04\x10@\x00\x01\x04\x10@\x00\x01\x04\x10@\x00\x01\x04\xb0R[\x04\x00\xc4\xd8kY\x0c`\xc7p|\x97\xefs\x1d\x80U\xc7/\x8af,\x00\x7f\xf3?U-?\xe0\x08\xfc\xa1\x98\xc9\x01\xd6\xf1\x97\xbae\x06X\xe0\xaf\x85\xe5\x05X\x00|\x01\x0f`\x01\xf0\x05<\x80\x05\xc0\x17\xf0\x00\x16\x00_@\x04t\x9c\xa8\xe7\x03L\x9c\xea\xc8\x06\xb0\xc0\xa9\xc2\x92\x01*N\xd6s\x01\x0cgsK\x05h8]O\x05\x080\x0f\x01\x1f0q\xa1#\x11`\xc7\x85F"\x80\xe3Bni\x00\x05\x97*i\x00\x15\x97\xaai\x00;.\xd5\xd2\x00\x06.\x15i\x00\xb1:\x00\x17c\x02\x04\x10@\x171\x1f0\xd2\x00\x1a.\xb5\xebUB/s\x9f2\xc7\x85<\xd1\xf7\xc0\xe0^\x02|\xc0\x81\x0bM*\x80\x7f\x0eE\xaa\xbf\x12\x1d\xa7k\\\x00\xff\x10X*\xc0\xd6\t\xff\xb5\xf4s\x97y#\x9aZ\xe0\xd0\x12\x13\x7f\x91O\xcb\xac$A\x98F\r4\xec\xf1\x87f\xe0\x0f\xc5A\x1c\xb7\xe1\x1f\x04\xef\xb6\xc6\xc8Y\x0b\xf2\xe6\xf3\xa7\x16\xe7\xee\xf8.\x1f\x87-67Z\xea>\x02\x00"Z-\xa6\xc1W\x01R%\x80\x00\x02\x08Pz8\x9e\x94G+7\x03f\xe0\xc9\xc5\xbc\x11`\x03\x84\x86\xdd\x05\xb0\x00\xa5\xb0{\x00\x16\x00_@\x04\x0c\xd0\x1aw\x00&\x88\xcd\x1b\x00\x01b\xc1\x07\x14P+t@\x03\xb5N\x07\x04\xa8\x05\x1d\xe0\xa0\xe6t\x00\xc8\t \x80\x00\xba\x8d\xbe)`\xac\xfe$\xde\xe9\x80\xbe\xfa\xab\xc4\x01j\x07\x1d`\xa0ft\xc06\xf8\xd70\x17PW\xff"3\x10\xb3\xc5?\xea\xdbv\x07\xa0\x80\x96Q\x01\xfcC\xd0\xee\xfa3\xe7\xa0\xf4jT\x00\xffi\xdc\xef\xfb;\x1d \x14\x1b\x13\xc0?\x89\xc2n\x04l\x0f<\xbb\xd7\xc7\xbd+4\x93\xff\x0c\xe6\x01\x08\x82\xd7y\xff"\xdf\xe1xZ^\xdeb\x95\xd2\x82\x7f\xfdR\x01\x9b5<\xa5f\x1b\x0f\xc0_\xac\xf4\x83\xb9N\xcc?\x08\xdd6>\x80G\x18\xc6_\xa9\xe7\x11\xbc\x95$\xb3\x12v\xe5Z\x88n\x99\x86=J;e\xf0^\xf2M\xab<\xeap\xfcC>\xea#\xed\xb8M\xa9\xedO\n\x8fV\x1f\x0bL\xee\x96\xd9\xdb\x18\x11\x9f(\x1e1\xf6\xd6\xe7a\x1ax\x12@\x00\x01\x04\x10@\x00\x01\x04\x10@\x00\x01\x04\x10@\x00\x01\x04\x10@\x00\x01\x04\x10@\x00\x01\x04\x10\xe0\xcd\x13@\x00\x01\x16\xef\xbf\xff\x01\xf0\x16\xb7@\x05\x81\xe7\\\x00\x00\x00\x00IEND\xaeB`\x82'
|
|
|
|
|
|
def _uuid_to_bytes(val):
|
|
if val is None:
|
|
return None
|
|
if isinstance(val, bytes):
|
|
return val
|
|
if isinstance(val, str):
|
|
try:
|
|
return uuid.UUID(val).bytes
|
|
except ValueError:
|
|
return None
|
|
if isinstance(val, uuid.UUID):
|
|
return val.bytes
|
|
return None
|
|
|
|
|
|
def _uuid_to_str(val):
|
|
if val is None:
|
|
return None
|
|
if isinstance(val, str):
|
|
return val
|
|
if isinstance(val, bytes) and len(val) == 16:
|
|
return str(uuid.UUID(bytes=val))
|
|
if isinstance(val, uuid.UUID):
|
|
return str(val)
|
|
return str(val)
|
|
|
|
|
|
class UUIDObj(str):
|
|
def __new__(cls, value):
|
|
if isinstance(value, bytes) and len(value) == 16:
|
|
uuid_str = str(uuid.UUID(bytes=value))
|
|
elif isinstance(value, str):
|
|
uuid_str = str(uuid.UUID(value))
|
|
elif isinstance(value, uuid.UUID):
|
|
uuid_str = str(value)
|
|
else:
|
|
raise ValueError(f"Invalid UUID: {value}")
|
|
return str.__new__(cls, uuid_str)
|
|
|
|
def __init__(self, value):
|
|
if isinstance(value, bytes) and len(value) == 16:
|
|
self._uuid = uuid.UUID(bytes=value)
|
|
elif isinstance(value, str):
|
|
self._uuid = uuid.UUID(value)
|
|
elif isinstance(value, uuid.UUID):
|
|
self._uuid = value
|
|
else:
|
|
raise ValueError(f"Invalid UUID: {value}")
|
|
|
|
@property
|
|
def ok(self):
|
|
return True
|
|
|
|
@property
|
|
def Bytes(self):
|
|
return self._uuid.bytes
|
|
|
|
@property
|
|
def bytes(self):
|
|
return self._uuid.bytes
|
|
|
|
@property
|
|
def LowerStr(self):
|
|
return str(self._uuid).lower()
|
|
|
|
@property
|
|
def UpperStr(self):
|
|
return str(self._uuid).upper().replace('-', '')
|
|
|
|
|
|
def NewUUID():
|
|
return uuid.uuid4()
|
|
|
|
|
|
def NewBUUID():
|
|
return uuid.uuid4().bytes
|
|
|
|
|
|
def Get16toBytes(val):
|
|
if val is None:
|
|
return None
|
|
if isinstance(val, bytes) and len(val) == 16:
|
|
return val
|
|
if isinstance(val, str):
|
|
try:
|
|
return uuid.UUID(val).bytes
|
|
except ValueError:
|
|
return None
|
|
if isinstance(val, uuid.UUID):
|
|
return val.bytes
|
|
return None
|
|
|
|
|
|
def GetBytesTo16(val):
|
|
if val is None:
|
|
return None
|
|
if isinstance(val, bytes) and len(val) == 16:
|
|
return str(uuid.UUID(bytes=val)).upper().replace('-', '')
|
|
if isinstance(val, str):
|
|
return val
|
|
if isinstance(val, uuid.UUID):
|
|
return str(val).upper().replace('-', '')
|
|
return str(val)
|
|
|
|
|
|
def GenerateUUID():
|
|
return uuid.uuid4().bytes
|
|
|
|
|
|
def openr(filepath):
|
|
try:
|
|
with open(filepath, 'r', encoding='utf-8') as f:
|
|
return f.read()
|
|
except Exception:
|
|
return ''
|
|
|
|
|
|
def openw(filepath, content):
|
|
try:
|
|
with open(filepath, 'w', encoding='utf-8') as f:
|
|
f.write(content)
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def opena(filepath, content):
|
|
try:
|
|
with open(filepath, 'a', encoding='utf-8') as f:
|
|
f.write(content)
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def JoinPath(*args):
|
|
return os.path.join(*args)
|
|
|
|
|
|
def Is_UUID(u):
|
|
try:
|
|
uuid.UUID(str(u))
|
|
return True
|
|
except (ValueError, AttributeError):
|
|
return False
|
|
|
|
|
|
def GetFileSha256(filepath):
|
|
h = hashlib.sha256()
|
|
try:
|
|
with open(filepath, 'rb') as f:
|
|
while chunk := f.read(8192):
|
|
h.update(chunk)
|
|
return h.hexdigest()
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
class Cookie:
|
|
_local = threading.local()
|
|
|
|
@staticmethod
|
|
def _get_pending():
|
|
if not hasattr(Cookie._local, 'pending'):
|
|
Cookie._local.pending = []
|
|
return Cookie._local.pending
|
|
|
|
@staticmethod
|
|
def get(name, default=None):
|
|
req = _get_current_request()
|
|
if req:
|
|
return req.COOKIES.get(name, default)
|
|
return default
|
|
|
|
@staticmethod
|
|
def set(name, value, domain='.gvsds.com', max_age=30*24*60*60, path='/', httponly=False, samesite='None', secure=True):
|
|
Cookie._get_pending().append({
|
|
'name': name, 'value': value, 'domain': domain,
|
|
'max_age': max_age, 'path': path, 'httponly': httponly, 'samesite': samesite, 'secure': secure,
|
|
})
|
|
|
|
@staticmethod
|
|
def delete(name, path='/', domain='.gvsds.com'):
|
|
Cookie._get_pending().append({
|
|
'name': name, 'value': '', 'max_age': 0, 'path': path, 'domain': domain, 'secure': True, 'samesite': 'None',
|
|
})
|
|
|
|
@staticmethod
|
|
def clear():
|
|
Cookie._get_pending().clear()
|
|
|
|
@staticmethod
|
|
def _apply_to_response(response):
|
|
pending = Cookie._get_pending()
|
|
for cookie in pending:
|
|
response.set_cookie(
|
|
cookie['name'],
|
|
cookie.get('value', ''),
|
|
max_age=cookie.get('max_age', 30*24*60*60),
|
|
path=cookie.get('path', '/'),
|
|
domain=cookie.get('domain', '.gvsds.com'),
|
|
secure=cookie.get('secure', True),
|
|
httponly=cookie.get('httponly', False),
|
|
samesite=cookie.get('samesite', 'None'),
|
|
)
|
|
pending.clear()
|
|
|
|
|
|
class IP:
|
|
@staticmethod
|
|
def Get(request=None):
|
|
if request:
|
|
xff = request.META.get('HTTP_X_FORWARDED_FOR', '')
|
|
if xff:
|
|
return xff.split(',')[0].strip()
|
|
return request.META.get('REMOTE_ADDR', '')
|
|
return '127.0.0.1'
|
|
|
|
|
|
class UGM:
|
|
@staticmethod
|
|
def GetELTUser(elt=None):
|
|
try:
|
|
request = _get_current_request()
|
|
if not request:
|
|
return None
|
|
elt_token = request.COOKIES.get('ELToken')
|
|
if not elt_token:
|
|
return None
|
|
elt_uuid = _uuid_to_bytes(elt_token)
|
|
elt_obj = EphemeralToken.objects.filter(elt_uuid=elt_uuid, elt_status=1).first()
|
|
if not elt_obj or elt_obj.is_expired():
|
|
return None
|
|
user = User.objects.filter(user_uuid=elt_obj.elt_user_uuid).first()
|
|
return user
|
|
except Exception as e:
|
|
logger.exception("GetELTUser failed")
|
|
return None
|
|
|
|
@staticmethod
|
|
def GetUserDataByUserUUID(user_uuid):
|
|
try:
|
|
return User.objects.get(user_uuid=_uuid_to_bytes(user_uuid))
|
|
except User.DoesNotExist:
|
|
return None
|
|
|
|
@staticmethod
|
|
def GetUserDataByUserName(username):
|
|
try:
|
|
return User.objects.get(user_name=username)
|
|
except User.DoesNotExist:
|
|
return None
|
|
|
|
@staticmethod
|
|
def CreateELT(user, MaxTime=30, Type=0, AllowIP=None):
|
|
elt = EphemeralToken.objects.create(
|
|
elt_uuid=NewBUUID(),
|
|
elt_user_uuid=user.user_uuid if hasattr(user, 'user_uuid') else _uuid_to_bytes(user),
|
|
elt_end_time=datetime.datetime.utcnow() + datetime.timedelta(days=MaxTime),
|
|
elt_type=Type,
|
|
elt_status=1,
|
|
)
|
|
return elt
|
|
|
|
@staticmethod
|
|
def GetUserAvatar(user):
|
|
try:
|
|
user_uuid = user.user_uuid if hasattr(user, 'user_uuid') else _uuid_to_bytes(user)
|
|
return UserAvatar.objects.get(user_uuid=user_uuid)
|
|
except UserAvatar.DoesNotExist:
|
|
return None
|
|
|
|
@staticmethod
|
|
def VerifyELT(ELT=None):
|
|
try:
|
|
request = _get_current_request()
|
|
if not request:
|
|
return False
|
|
elt_token = request.COOKIES.get('ELToken')
|
|
if not elt_token:
|
|
return False
|
|
elt_uuid = _uuid_to_bytes(elt_token)
|
|
elt_obj = EphemeralToken.objects.filter(elt_uuid=elt_uuid, elt_status=1).first()
|
|
if not elt_obj or elt_obj.is_expired():
|
|
return False
|
|
return elt_obj
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def PermissionVerifier(permission, attributes):
|
|
if not permission:
|
|
return True
|
|
user = UGM.GetELTUser()
|
|
if not user:
|
|
return False
|
|
from gvsds.apps.gerp.models import UserABAC
|
|
for attr in attributes:
|
|
abac_match = UserABAC.objects.filter(
|
|
user_uuid=user.user_uuid,
|
|
hierarchical_permission_identifiers=attr
|
|
).first()
|
|
if abac_match:
|
|
continue
|
|
rbac_result = user.has_permission(attr)
|
|
if rbac_result:
|
|
continue
|
|
return False
|
|
return True
|
|
|
|
|
|
_request_local = threading.local()
|
|
|
|
|
|
def _get_query_cache():
|
|
if not hasattr(_request_local, 'query_cache'):
|
|
_request_local.query_cache = {}
|
|
return _request_local.query_cache
|
|
|
|
|
|
def _invalidate_query_cache(model_class=None):
|
|
cache = getattr(_request_local, 'query_cache', None)
|
|
if cache is None:
|
|
return
|
|
if model_class is None:
|
|
cache.clear()
|
|
else:
|
|
keys_to_remove = [k for k in cache if k[0] == model_class]
|
|
for k in keys_to_remove:
|
|
del cache[k]
|
|
|
|
|
|
def _get_current_request():
|
|
return getattr(_request_local, 'request', None)
|
|
|
|
def set_current_request(request):
|
|
_request_local.request = request
|
|
|
|
|
|
def _get_db_session():
|
|
session = getattr(_request_local, 'db_session', None)
|
|
if session is None:
|
|
session = _DBSession()
|
|
_request_local.db_session = session
|
|
return session
|
|
|
|
|
|
class _AggregateQuery:
|
|
def __init__(self, model, *aggregates):
|
|
self.model = model
|
|
self.aggregates = aggregates
|
|
self._filters_q = []
|
|
self._filters_kwargs = {}
|
|
|
|
def filter(self, *args, **kwargs):
|
|
converted_kwargs = {}
|
|
for key, val in kwargs.items():
|
|
parts = key.split('__')
|
|
parts[0] = _pascal_to_snake(parts[0])
|
|
converted_kwargs['__'.join(parts)] = val
|
|
self._filters_q.extend(args)
|
|
self._filters_kwargs.update(converted_kwargs)
|
|
return self
|
|
|
|
def scalar(self):
|
|
try:
|
|
qs = self.model.objects.filter(*self._filters_q, **self._filters_kwargs)
|
|
agg_names = [f'_agg_{i}' for i in range(len(self.aggregates))]
|
|
result = qs.aggregate(**dict(zip(agg_names, self.aggregates)))
|
|
if len(self.aggregates) == 1:
|
|
return result.get(agg_names[0])
|
|
return tuple(result[n] for n in agg_names)
|
|
except Exception:
|
|
return None
|
|
|
|
def all(self):
|
|
return self.scalar()
|
|
|
|
def first(self):
|
|
return self.scalar()
|
|
|
|
|
|
class _MultiTableRow:
|
|
def __init__(self, values, field_names):
|
|
for name, val in zip(field_names, values):
|
|
setattr(self, name, val)
|
|
|
|
|
|
class _MultiTableQuery:
|
|
def __init__(self, model, select_fields=None):
|
|
self.model = model
|
|
self._select_fields = select_fields or []
|
|
self._joins = []
|
|
self._outerjoins = []
|
|
self._filters_q = []
|
|
self._filters_kwargs = {}
|
|
|
|
def join(self, target, condition=None):
|
|
self._joins.append((target, condition))
|
|
return self
|
|
|
|
def outerjoin(self, target, condition=None):
|
|
self._outerjoins.append((target, condition))
|
|
return self
|
|
|
|
def filter(self, *args, **kwargs):
|
|
self._filters_q.extend(args)
|
|
self._filters_kwargs.update(kwargs)
|
|
return self
|
|
|
|
def _remap_q(self, q):
|
|
from django.db.models import Q as DjQ
|
|
new_children = []
|
|
for child in q.children:
|
|
if isinstance(child, DjQ):
|
|
new_children.append(self._remap_q(child))
|
|
elif isinstance(child, tuple):
|
|
key, val = child
|
|
new_key = self._remap_key(key)
|
|
new_children.append((new_key, val))
|
|
else:
|
|
new_children.append(child)
|
|
if new_children:
|
|
new_q = DjQ(*new_children)
|
|
new_q.connector = q.connector
|
|
new_q.negated = q.negated
|
|
return new_q
|
|
return q
|
|
|
|
def _remap_key(self, key):
|
|
parts = key.split('__')
|
|
field_name = parts[0]
|
|
try:
|
|
self.model._meta.get_field(field_name)
|
|
return key
|
|
except Exception:
|
|
pass
|
|
for joined_model, _ in self._joins + self._outerjoins:
|
|
if not isinstance(joined_model, type):
|
|
continue
|
|
rel_path = self._find_relation_path(joined_model)
|
|
if rel_path is None:
|
|
continue
|
|
base_field = field_name
|
|
if base_field.endswith('_id'):
|
|
base_field = base_field[:-3]
|
|
try:
|
|
joined_model._meta.get_field(base_field)
|
|
if len(parts) > 1:
|
|
return f'{rel_path}__{base_field}__{"__".join(parts[1:])}'
|
|
return f'{rel_path}__{base_field}'
|
|
except Exception:
|
|
pass
|
|
for f in joined_model._meta.local_fields:
|
|
pascal = snake_to_pascal(f.name)
|
|
if pascal == field_name or pascal == base_field:
|
|
if len(parts) > 1:
|
|
return f'{rel_path}__{f.name}__{"__".join(parts[1:])}'
|
|
return f'{rel_path}__{f.name}'
|
|
return key
|
|
|
|
def _build_queryset(self):
|
|
qs = self.model.objects.all()
|
|
select_related_fields = []
|
|
for target, condition in self._joins + self._outerjoins:
|
|
if isinstance(target, type) and issubclass(target, django.db.models.Model):
|
|
rel_path = self._find_relation_path(target)
|
|
if rel_path is None:
|
|
continue
|
|
is_forward_fk = False
|
|
for f in self.model._meta.local_fields:
|
|
if hasattr(f, 'related_model') and f.related_model == target and f.name == rel_path:
|
|
is_forward_fk = True
|
|
break
|
|
if is_forward_fk:
|
|
select_related_fields.append(rel_path)
|
|
if select_related_fields:
|
|
qs = qs.select_related(*select_related_fields)
|
|
remapped_q = []
|
|
for q in self._filters_q:
|
|
from django.db.models import Q as DjQ
|
|
if isinstance(q, DjQ):
|
|
remapped_q.append(self._remap_q(q))
|
|
else:
|
|
remapped_q.append(q)
|
|
remapped_kwargs = {}
|
|
for k, v in self._filters_kwargs.items():
|
|
remapped_kwargs[self._remap_key(k)] = v
|
|
if remapped_q or remapped_kwargs:
|
|
qs = qs.filter(*remapped_q, **remapped_kwargs)
|
|
return qs
|
|
|
|
def _find_relation_path(self, target_model):
|
|
for rel in self.model._meta.get_fields():
|
|
if hasattr(rel, 'related_model') and rel.related_model == target_model:
|
|
if hasattr(rel, 'field'):
|
|
return rel.name
|
|
if hasattr(rel, 'get_accessor_name'):
|
|
return rel.get_accessor_name()
|
|
for f in self.model._meta.local_fields:
|
|
if hasattr(f, 'related_model') and f.related_model == target_model:
|
|
return f.name
|
|
return None
|
|
|
|
def _get_field_names(self):
|
|
names = []
|
|
for f in self._select_fields:
|
|
raw_name = None
|
|
if hasattr(f, '_django_name'):
|
|
raw_name = f._django_name
|
|
elif hasattr(f, 'field_name'):
|
|
raw_name = f.field_name
|
|
elif hasattr(f, '__name__'):
|
|
raw_name = f.__name__
|
|
else:
|
|
raw_name = str(f)
|
|
if raw_name.endswith('_id'):
|
|
pascal = snake_to_pascal(raw_name[:-3])
|
|
else:
|
|
pascal = snake_to_pascal(raw_name)
|
|
names.append(pascal)
|
|
return names
|
|
|
|
def _resolve_field_on_obj(self, obj, django_field):
|
|
val = getattr(obj, django_field, None)
|
|
if val is not None:
|
|
return self._convert_uuid_field(obj, django_field, val)
|
|
for f in self._select_fields:
|
|
if hasattr(f, '_django_name') and f._django_name == django_field:
|
|
if hasattr(f, 'model_class') and f.model_class != self.model:
|
|
target_model = f.model_class
|
|
rel_path = self._find_relation_path(target_model)
|
|
if rel_path:
|
|
related_obj = getattr(obj, rel_path, None)
|
|
if related_obj is not None:
|
|
val = getattr(related_obj, django_field, None)
|
|
if val is not None:
|
|
return self._convert_uuid_field(related_obj, django_field, val)
|
|
break
|
|
return None
|
|
|
|
def _convert_uuid_field(self, obj, field_name, value):
|
|
try:
|
|
field = obj._meta.get_field(field_name)
|
|
from gvsds.apps.core.models import UUIDField
|
|
if isinstance(field, UUIDField) and isinstance(value, str):
|
|
return uuid.UUID(value).bytes
|
|
except Exception:
|
|
pass
|
|
if field_name.endswith('_id') and isinstance(value, str):
|
|
try:
|
|
uuid.UUID(value)
|
|
return uuid.UUID(value).bytes
|
|
except (ValueError, AttributeError):
|
|
pass
|
|
return value
|
|
|
|
def all(self):
|
|
qs = self._build_queryset()
|
|
if self._select_fields:
|
|
field_names = self._get_field_names()
|
|
django_fields = []
|
|
for f in self._select_fields:
|
|
if hasattr(f, '_django_name'):
|
|
django_fields.append(f._django_name)
|
|
elif hasattr(f, 'field_name'):
|
|
django_fields.append(f.field_name)
|
|
else:
|
|
django_fields.append(str(f))
|
|
results = []
|
|
for obj in qs:
|
|
values = [self._resolve_field_on_obj(obj, df) for df in django_fields]
|
|
results.append(_MultiTableRow(values, field_names))
|
|
return results
|
|
return list(qs)
|
|
|
|
def first(self):
|
|
qs = self._build_queryset()
|
|
obj = qs.first()
|
|
if obj is None:
|
|
return None
|
|
if self._select_fields:
|
|
field_names = self._get_field_names()
|
|
django_fields = []
|
|
for f in self._select_fields:
|
|
if hasattr(f, '_django_name'):
|
|
django_fields.append(f._django_name)
|
|
elif hasattr(f, 'field_name'):
|
|
django_fields.append(f.field_name)
|
|
else:
|
|
django_fields.append(str(f))
|
|
values = [self._resolve_field_on_obj(obj, df) for df in django_fields]
|
|
return _MultiTableRow(values, field_names)
|
|
return obj
|
|
|
|
|
|
class _DeleteStmt:
|
|
def __init__(self, model_class, conditions=None):
|
|
self.model_class = model_class
|
|
self.conditions = conditions or Q()
|
|
|
|
def where(self, *args, **kwargs):
|
|
from django.db.models import Q
|
|
new_conditions = Q()
|
|
for a in args:
|
|
if isinstance(a, Q):
|
|
new_conditions &= a
|
|
self.conditions = new_conditions
|
|
return self
|
|
|
|
def execute(self):
|
|
if self.model_class and self.conditions:
|
|
self.model_class.objects.filter(self.conditions).delete()
|
|
elif self.model_class:
|
|
self.model_class.objects.all().delete()
|
|
|
|
|
|
class _DBSession:
|
|
def __init__(self):
|
|
self._pending_adds = []
|
|
self._pending_deletes = []
|
|
self._tracked_instances = set()
|
|
|
|
def add(self, obj):
|
|
logger.debug(f"[DBSession.add] {obj.__class__.__name__} pk={getattr(obj, 'pk', '?')}")
|
|
self._pending_adds.append(obj)
|
|
|
|
def delete(self, obj):
|
|
logger.debug(f"[DBSession.delete] {obj.__class__.__name__} pk={obj.pk}")
|
|
self._pending_deletes.append(obj)
|
|
self._tracked_instances.discard(obj)
|
|
|
|
def track(self, obj):
|
|
if isinstance(obj, django.db.models.Model):
|
|
self._tracked_instances.add(obj)
|
|
|
|
def commit(self):
|
|
from django.db import transaction
|
|
logger.debug(f"[DBSession.commit] deletes={len(self._pending_deletes)} adds={len(self._pending_adds)} tracked={len(self._tracked_instances)}")
|
|
delete_pks = {(obj.__class__, obj.pk) for obj in self._pending_deletes if obj.pk is not None}
|
|
with transaction.atomic():
|
|
for obj in self._pending_deletes:
|
|
pk = obj.pk
|
|
model_name = obj.__class__.__name__
|
|
result = obj.delete()
|
|
logger.debug(f"[DBSession.commit] DELETE {model_name} pk={pk} result={result}")
|
|
for obj in self._pending_adds:
|
|
obj.save()
|
|
for obj in self._tracked_instances:
|
|
if obj.pk is not None and (obj.__class__, obj.pk) in delete_pks:
|
|
logger.debug(f"[DBSession.commit] SKIP save for deleted {obj.__class__.__name__} pk={obj.pk}")
|
|
continue
|
|
try:
|
|
obj.save()
|
|
except Exception as e:
|
|
logger.exception(f"[DBSession.commit] Failed to save {obj.__class__.__name__} pk={obj.pk}")
|
|
self._pending_adds.clear()
|
|
self._pending_deletes.clear()
|
|
self._tracked_instances.clear()
|
|
_invalidate_query_cache()
|
|
|
|
def rollback(self):
|
|
self._pending_adds.clear()
|
|
self._pending_deletes.clear()
|
|
self._tracked_instances.clear()
|
|
_invalidate_query_cache()
|
|
|
|
def execute(self, stmt):
|
|
if isinstance(stmt, _DeleteStmt):
|
|
stmt.execute()
|
|
elif isinstance(stmt, str):
|
|
from django.db import connection
|
|
with connection.cursor() as cursor:
|
|
cursor.execute(stmt)
|
|
|
|
def query(self, *args):
|
|
aggregates = []
|
|
model = None
|
|
select_fields = []
|
|
for a in args:
|
|
try:
|
|
from gvsds.fsr import _AggregateExpr
|
|
if isinstance(a, _AggregateExpr):
|
|
if a.model:
|
|
model = a.model
|
|
aggregates.append(a.aggregate)
|
|
continue
|
|
except Exception:
|
|
pass
|
|
from django.db.models import Aggregate
|
|
if isinstance(a, Aggregate):
|
|
aggregates.append(a)
|
|
elif isinstance(a, type) and issubclass(a, django.db.models.Model):
|
|
model = a
|
|
elif hasattr(a, '_django_name'):
|
|
from gvsds.model_base import FieldExpression
|
|
if isinstance(a, FieldExpression):
|
|
if a.model_class and model is None:
|
|
model = a.model_class
|
|
select_fields.append(a)
|
|
if aggregates:
|
|
return _AggregateQuery(model, *aggregates)
|
|
if select_fields:
|
|
return _MultiTableQuery(model, select_fields)
|
|
if model:
|
|
return _QueryCompat(model)
|
|
return None
|
|
|
|
|
|
class _DB:
|
|
@property
|
|
def func(self):
|
|
from gvsds.fsr import _CompatFunc
|
|
if not hasattr(self, '_func_instance'):
|
|
self._func_instance = _CompatFunc()
|
|
return self._func_instance
|
|
|
|
Float = float
|
|
String = str
|
|
|
|
@property
|
|
def session(self):
|
|
from gvsds.fluent import _get_session
|
|
return _get_session()
|
|
|
|
@staticmethod
|
|
def or_(*args):
|
|
from django.db.models import Q
|
|
result = None
|
|
for q in args:
|
|
if isinstance(q, Q):
|
|
if result is None:
|
|
result = q
|
|
else:
|
|
result |= q
|
|
return result if result is not None else Q()
|
|
|
|
@staticmethod
|
|
def and_(*args):
|
|
from django.db.models import Q
|
|
result = Q()
|
|
for q in args:
|
|
if isinstance(q, Q):
|
|
result &= q
|
|
return result
|
|
|
|
|
|
class _ValuesQueryCompat:
|
|
def __init__(self, qs, fields):
|
|
self._qs = qs
|
|
self._fields = fields
|
|
|
|
def filter(self, *args, **kwargs):
|
|
self._qs = self._qs.filter(*args, **kwargs)
|
|
return self
|
|
|
|
def first(self):
|
|
obj = self._qs.first()
|
|
if obj is None:
|
|
return None
|
|
if len(self._fields) == 1:
|
|
return getattr(obj, 'val0', None)
|
|
return tuple(getattr(obj, f'val{i}', None) for i in range(len(self._fields)))
|
|
|
|
def all(self):
|
|
results = []
|
|
for obj in self._qs:
|
|
if len(self._fields) == 1:
|
|
results.append(getattr(obj, 'val0', None))
|
|
else:
|
|
results.append(tuple(getattr(obj, f'val{i}', None) for i in range(len(self._fields))))
|
|
return results
|
|
|
|
def scalar(self):
|
|
obj = self._qs.first()
|
|
if obj is None:
|
|
return None
|
|
return getattr(obj, 'val0', None)
|
|
|
|
|
|
class _QueryCompat:
|
|
def __init__(self, model_class):
|
|
self.model = model_class
|
|
self._qs = model_class.objects.all()
|
|
self._joined_models = []
|
|
self._patched_fks = []
|
|
self._cache_enabled = True
|
|
|
|
def _cache_key(self, method):
|
|
qs = self._qs
|
|
try:
|
|
query_str = str(qs.query)
|
|
except Exception:
|
|
query_str = id(qs)
|
|
return (self.model, method, query_str)
|
|
|
|
def _ensure_reverse_relation(self, joined_model):
|
|
for fk in joined_model._meta.local_fields:
|
|
if not hasattr(fk, 'remote_field') or not fk.remote_field:
|
|
continue
|
|
if fk.remote_field.related_name == '+' and fk.remote_field.model == self.model:
|
|
rel_name = joined_model._meta.model_name
|
|
fk.remote_field.related_name = rel_name
|
|
self._patched_fks.append((fk, '+'))
|
|
|
|
def _restore_fks(self):
|
|
for fk, original in self._patched_fks:
|
|
fk.remote_field.related_name = original
|
|
self._patched_fks.clear()
|
|
|
|
def _convert_kwargs(self, kwargs):
|
|
django_kwargs = {}
|
|
for key, val in kwargs.items():
|
|
if key.endswith('__isnull'):
|
|
django_kwargs[key] = val
|
|
else:
|
|
parts = key.split('__')
|
|
parts[0] = _pascal_to_snake(parts[0])
|
|
django_kwargs['__'.join(parts)] = val
|
|
return django_kwargs
|
|
|
|
def _find_rel_path(self, field_name):
|
|
try:
|
|
self.model._meta.get_field(field_name)
|
|
return None
|
|
except Exception:
|
|
pass
|
|
lookup_name = field_name[:-3] if field_name.endswith('_id') else None
|
|
for joined in self._joined_models:
|
|
found = False
|
|
try:
|
|
joined._meta.get_field(field_name)
|
|
found = True
|
|
except Exception:
|
|
if lookup_name:
|
|
try:
|
|
joined._meta.get_field(lookup_name)
|
|
found = True
|
|
except Exception:
|
|
pass
|
|
if not found:
|
|
continue
|
|
for rel in self.model._meta.get_fields(include_hidden=True):
|
|
if hasattr(rel, 'related_model') and rel.related_model == joined:
|
|
if hasattr(rel, 'get_accessor_name'):
|
|
acc = rel.get_accessor_name()
|
|
if acc != '+':
|
|
return acc
|
|
if hasattr(rel, 'name') and rel.name != '+':
|
|
return rel.name
|
|
return joined._meta.model_name
|
|
return None
|
|
|
|
def _remap_q(self, q):
|
|
from django.db.models import Q as DjQ
|
|
new_children = []
|
|
for child in q.children:
|
|
if isinstance(child, DjQ):
|
|
new_children.append(self._remap_q(child))
|
|
elif isinstance(child, tuple):
|
|
key, val = child
|
|
new_key = self._remap_key(key)
|
|
new_children.append((new_key, val))
|
|
else:
|
|
new_children.append(child)
|
|
if new_children:
|
|
new_q = DjQ(*new_children)
|
|
new_q.connector = q.connector
|
|
new_q.negated = q.negated
|
|
return new_q
|
|
return q
|
|
|
|
def _remap_key(self, key):
|
|
parts = key.split('__')
|
|
field_name = parts[0]
|
|
try:
|
|
self.model._meta.get_field(field_name)
|
|
return key
|
|
except Exception:
|
|
pass
|
|
rel_path = self._find_rel_path(field_name)
|
|
if rel_path:
|
|
return rel_path + '__' + key
|
|
return key
|
|
|
|
def filter(self, *args, **kwargs):
|
|
converted_args = []
|
|
for arg in args:
|
|
from django.db.models import Q as DjQ
|
|
if isinstance(arg, DjQ) and self._joined_models:
|
|
converted_args.append(self._remap_q(arg))
|
|
else:
|
|
converted_args.append(arg)
|
|
django_kwargs = self._convert_kwargs(kwargs)
|
|
self._qs = self._qs.filter(*converted_args, **django_kwargs)
|
|
return self
|
|
|
|
def order_by(self, *args):
|
|
converted = []
|
|
for a in args:
|
|
if isinstance(a, str):
|
|
if a.startswith('-'):
|
|
converted.append('-' + _pascal_to_snake(a[1:]))
|
|
else:
|
|
converted.append(_pascal_to_snake(a))
|
|
else:
|
|
converted.append(str(a))
|
|
self._qs = self._qs.order_by(*converted)
|
|
return self
|
|
|
|
def first(self):
|
|
if self._cache_enabled:
|
|
cache = _get_query_cache()
|
|
key = self._cache_key('first')
|
|
if key in cache:
|
|
return cache[key]
|
|
obj = self._qs.first()
|
|
if obj is not None:
|
|
_get_db_session().track(obj)
|
|
if self._cache_enabled:
|
|
_get_query_cache()[self._cache_key('first')] = obj
|
|
return obj
|
|
|
|
def all(self):
|
|
if self._cache_enabled:
|
|
cache = _get_query_cache()
|
|
key = self._cache_key('all')
|
|
if key in cache:
|
|
return cache[key]
|
|
results = list(self._qs)
|
|
session = _get_db_session()
|
|
for obj in results:
|
|
session.track(obj)
|
|
if self._cache_enabled:
|
|
_get_query_cache()[self._cache_key('all')] = results
|
|
return results
|
|
|
|
def count(self):
|
|
if self._cache_enabled:
|
|
cache = _get_query_cache()
|
|
key = self._cache_key('count')
|
|
if key in cache:
|
|
return cache[key]
|
|
result = self._qs.count()
|
|
if self._cache_enabled:
|
|
_get_query_cache()[self._cache_key('count')] = result
|
|
return result
|
|
|
|
def get(self, **kwargs):
|
|
django_kwargs = self._convert_kwargs(kwargs)
|
|
obj = self._qs.get(**django_kwargs)
|
|
_get_db_session().track(obj)
|
|
return obj
|
|
|
|
def limit(self, n):
|
|
self._qs = self._qs[:n]
|
|
return self
|
|
|
|
def offset(self, n):
|
|
self._qs = self._qs[n:]
|
|
return self
|
|
|
|
def join(self, *args, **kwargs):
|
|
for arg in args:
|
|
if isinstance(arg, type) and issubclass(arg, django.db.models.Model):
|
|
if arg not in self._joined_models:
|
|
self._joined_models.append(arg)
|
|
self._ensure_reverse_relation(arg)
|
|
return self
|
|
|
|
def values(self, *fields):
|
|
qs = self._qs
|
|
annotations = {}
|
|
for i, f in enumerate(fields):
|
|
from gvsds.fsr import _AggregateExpr
|
|
if isinstance(f, _AggregateExpr):
|
|
annotations[f'val{i}'] = f.aggregate
|
|
elif isinstance(f, (django.db.models.Aggregate, django.db.models.Expression)):
|
|
annotations[f'val{i}'] = f
|
|
if annotations:
|
|
qs = qs.annotate(**annotations)
|
|
return _ValuesQueryCompat(qs, fields)
|
|
|
|
def update(self, values):
|
|
if isinstance(values, dict):
|
|
converted = {}
|
|
for k, v in values.items():
|
|
converted[_pascal_to_snake(k)] = v
|
|
return self._qs.update(**converted)
|
|
return 0
|
|
|
|
def delete(self):
|
|
return self._qs.delete()
|
|
|
|
def params(self, **kwargs):
|
|
return self
|
|
|
|
def paginate(self, page=1, per_page=20, error_out=False):
|
|
from django.core.paginator import Paginator
|
|
if not self._qs.ordered:
|
|
meta = getattr(self.model, '_meta', None)
|
|
if meta and meta.pk:
|
|
self._qs = self._qs.order_by(meta.pk.name)
|
|
paginator = Paginator(self._qs, per_page)
|
|
try:
|
|
page_obj = paginator.page(page)
|
|
except Exception:
|
|
page_obj = paginator.page(1)
|
|
page_obj.total = paginator.count
|
|
page_obj.items = page_obj.object_list
|
|
page_obj.pages = paginator.num_pages
|
|
page_obj.page = page
|
|
page_obj.per_page = per_page
|
|
page_obj.has_next = page_obj.has_next()
|
|
page_obj.has_prev = page_obj.has_previous()
|
|
page_obj.next_num = page_obj.next_page_number() if page_obj.has_next else None
|
|
page_obj.prev_num = page_obj.previous_page_number() if page_obj.has_prev else None
|
|
return page_obj
|
|
|
|
|
|
def _add_query_compat_to_models():
|
|
from gvsds.apps.crb.models import Enterprise, EnterpriseUser
|
|
for model in [User, Company, Department, Role, UserRole, RoleDept,
|
|
Permission, FileAsset, WorkOrder, Message, Menu,
|
|
Tenant, CompanyInfo, Enterprise, EnterpriseUser]:
|
|
if not hasattr(model, 'query'):
|
|
model.query = type('query', (), {
|
|
'__getattr__': lambda self, name: getattr(_QueryCompat(model), name)
|
|
})()
|
|
|
|
|
|
db = _DB()
|
|
|
|
GetELTUser = UGM.GetELTUser
|
|
|
|
|
|
def Gender2Str(c):
|
|
if c == -1:
|
|
return "未更改"
|
|
elif c == 1:
|
|
return "男"
|
|
elif c == 0:
|
|
return "女"
|
|
else:
|
|
return "错误"
|
|
|
|
|
|
import smtplib
|
|
import email.mime.text
|
|
import email.mime.multipart
|
|
import email.header
|
|
import email.encoders
|
|
from email.mime.base import MIMEBase
|
|
|
|
|
|
class EmailSender:
|
|
def __init__(self, smtpServer, smtpPort, senderEmail, senderPassword, senderName=None):
|
|
self.SmtpServer = smtpServer
|
|
self.SmtpPort = smtpPort
|
|
self.SenderEmail = senderEmail
|
|
self.SenderPassword = senderPassword
|
|
self.SenderName = senderName if senderName else senderEmail
|
|
self.FromHeader = f"{email.header.Header(self.SenderName, 'utf-8')}"
|
|
|
|
def SendTextEmail(self, receiverEmails, subject, content):
|
|
msg = email.mime.text.MIMEText(content, 'plain', 'utf-8')
|
|
msg['From'] = self.FromHeader
|
|
msg['To'] = ','.join(receiverEmails)
|
|
msg['Subject'] = email.header.Header(subject, 'utf-8')
|
|
try:
|
|
with smtplib.SMTP_SSL(self.SmtpServer, self.SmtpPort) as server:
|
|
server.login(self.SenderEmail, self.SenderPassword)
|
|
server.sendmail(self.SenderEmail, receiverEmails, msg.as_string())
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"发送文本邮件失败: {str(e)}")
|
|
return False
|
|
|
|
def SendHtmlEmail(self, receiverEmails, subject, htmlContent):
|
|
msg = email.mime.text.MIMEText(htmlContent, 'html', 'utf-8')
|
|
msg['From'] = self.FromHeader
|
|
msg['To'] = ','.join(receiverEmails)
|
|
msg['Subject'] = email.header.Header(subject, 'utf-8')
|
|
try:
|
|
with smtplib.SMTP_SSL(self.SmtpServer, self.SmtpPort) as server:
|
|
server.login(self.SenderEmail, self.SenderPassword)
|
|
server.sendmail(self.SenderEmail, receiverEmails, msg.as_string())
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"发送HTML邮件失败: {str(e)}")
|
|
return False
|
|
|
|
|
|
def NonLegalChar(s):
|
|
errors = r"\/:*?" + '"' + "<>!@#$%^&*()【】|、{}《》,。,."
|
|
for c in str(s):
|
|
if c in errors:
|
|
return True
|
|
return False
|
|
|
|
|
|
class BadH:
|
|
pass
|
|
|
|
|
|
class StoreValueRule:
|
|
pass
|