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