#!/usr/bin/env python # -*- coding:utf-8 -*- # # Author : RobertDing # E-mail : robertdingx@gmail.com # Date : 16/03/25 13:15:41 # Desc : view 等 基类 # import json from functools import wraps from distutils.version import LooseVersion from django.views.decorators.csrf import csrf_exempt from django.conf import settings from django.views.generic import View from django.views.decorators.csrf import ensure_csrf_cookie from django.shortcuts import render from django.http import JsonResponse from django.http.response import HttpResponseBase from helios.rpc.exceptions import RPCFaultException from gm_types.gaia import PLATFORM_CHOICES from gm_types.ascle.error import ERROR from utils.exception import SunException from utils.user_util import require_login from utils.logger import log_error _EMPTY_OBJECT = object() class LazyAttrDict(object): def __init__(self, query_dict, configs, request_version): self._configs = configs self._query_dict = query_dict self.request_version = self.request_version def __getattr__(self, attr): config = self._configs.get(attr) if config is None: # 若是没有写config直接返回值 return self._query_dict.get(attr) default = config.get('default', _EMPTY_OBJECT) raw = self._query_dict.get(attr, default) if raw is _EMPTY_OBJECT: if self.request_version: # 目前使用参数version来判断请求来源 version_add = config.get('version_add', '0.0.1') if LooseVersion(self.request_version) >= LooseVersion(version_add): version_deprecated = config.get('version_deprecated') if not version_deprecated or LooseVersion(self.request_version) < LooseVersion(version_deprecated): raise SunException(ERROR.ARG_MISS, '缺少参数: %s' % attr) else: raise SunException(ERROR.ARG_MISS, '缺少参数: %s' % attr) if raw is default: return default else: try: access = config.get('access', str) if access == bool: # 处理Ajax传Boolean if raw == 1 or raw == '1' or raw.lower() == 'true': raw = True else: raw = False else: raw = access(raw) except: raise SunException(ERROR.ARG_ERROR, '错误参数: %s' % attr) # 因为有'access': lambda s: s.strip()类似的情况,需要在转换之后去判断blank if raw == '' and not config.get("blank", True): # 提取出来的参数不允许为'' raise SunException(ERROR.ARG_ERROR, '参数不能为空: %s' % attr) return raw def get(self, k): return self.__getattr__(k) def to_dict(self, exclude=[]): """ 只转化 config 中配置的参数, 方便用于表单提交 @param exclude: 为排出不做提交的参数 """ p = {} for k in self._configs.keys(): if k not in exclude: p[k] = getattr(self, k) return p def __len__(self): return len(self._query_dict) def __iter__(self): return iter(self._query_dict) def handler_exception(fn): @wraps(fn) def _wrapper(view, request, *args, **kwargs): try: response = fn(view, request, *args, **kwargs) except Exception as exception: if settings.DEBUG and not isinstance(exception, (RPCFaultException, SunException)): log_error() raise if isinstance(exception, RPCFaultException): if exception.error == 401: # gaia返回的需要登录重新处理 return require_login(request, origin='RPCFaultException_401') elif exception.error == 404: log_error() exception = SunException(exception.error, exception.message) elif isinstance(exception, SunException): pass else: # 服务器错误 # 参照 http://git.gengmei.cc/backend/gm-types/blob/master/gm_types/ascle/error.py exception = SunException(ERROR.ASCLE_ERROR) log_error() data = { 'data': None, 'error': exception.code, 'message': exception.message, } response = JsonResponse(data) return response return _wrapper class APIView(View): """ API 基类 - 如果方法返回的数据是 HttpResponseBase 的子类则不做 JsonResponse 处理 处理 GET 与 POST 参数 要求子类显示声明要求的参数, 格式如下 ``` args_GET: { name: { 'access': int, # 处理参数并返回结果 'default': 0 # 不写default,则表示参数不能为None 'version_add': '2.8.0' # client 请求参数添加版本, 默认是APP兼容最低版本 'version_deprecated': '2.9.0' # client请求参数废弃版本,默认为None 'blank' : True, # 默认为True, 代表允许从request解析到参数允许为''或者' ' } } * version_add 和 version_deprecated 需要根据请求参数version去确认是否必须 args_POST: ... ``` """ args_GET = {} args_POST = {} decorators = [] template = None # 返回html页面 # 兼容起始版本,方法名后缀,低于版本号执行此方法, 从小到大排序 """ 1) 若compatible_versions = ['2.4.0', '2.8.0', ] 请求参数version<'2.4.0', 执行 get_240 / render_get_240 请求参数'2.4.0'<=version<'2.8.0', 执行 get_270 / render_get_270 请求参数version>='2.8.0', 执行 get / render_get 2) 其他method(post/delete)方法和get方法同理 3) 可参考示例:api/client/account.py ClientLoginView """ compatible_versions = [] @classmethod def as_view(cls, enable_csrf=True, **initkwargs): view = super().as_view(**initkwargs) if ensure_csrf_cookie not in cls.decorators: cls.decorators.insert(0, ensure_csrf_cookie) # 客户端需要添加忽略csrftoken的校验 if not enable_csrf and csrf_exempt not in cls.decorators: cls.decorators.insert(0, csrf_exempt) for deco in cls.decorators[::-1]: view = deco(view) return view def prepare(self, request, *args, **kwargs): self.request = request # 请求是否来自客户端, client js请求也会带上version的 self.request_version = request.GET.get('version') self.request_from_client = False self.args_default = ClientDefaultArgs(request.GET) self.args_get = LazyAttrDict(request.GET, self.args_GET, self.request_version) self.args_post = LazyAttrDict(request.POST, self.args_POST, self.request_version) self.rpc = request.rpc.origin @handler_exception def dispatch(self, request, *args, **kwargs): self.prepare(request, *args, **kwargs) version = None method_name = request.method.lower() if method_name in self.http_method_names: handler = getattr(self, method_name, None) if self.request_from_client: # 若是客户端请求,做兼容处理,带上版本号 version = self.args_default.get_min_verion(self.compatible_versions) if version: version = version.replace('.', '') client_handler = getattr(self, '{}_{}'.format(method_name, version), None) if callable(client_handler): handler = client_handler if callable(handler): data = handler(request, *args, **kwargs) else: raise SunException(ERROR.HTTP_METHOD_NOT_ALLOW) else: raise SunException(ERROR.HTTP_METHOD_NOT_ALLOW) if isinstance(data, HttpResponseBase): return data # 找是否有组织数据的函数 render_name = 'render_{}'.format(method_name) render_method = getattr(self, render_name, None) if version: # 若是客户端请求有兼容版本 client_render = getattr(self, '{}_{}'.format(render_name, version), None) if callable(client_render): render_method = client_render # 执行组织数据函数 if callable(render_method): if data is not None: data = render_method(data, *args, **kwargs) else: data = render_method(*args, **kwargs) if isinstance(data, HttpResponseBase): return data elif self.template is not None: if settings.DEBUG and self.args_get.debug: return JsonResponse(data, safe=False) else: return render(request, self.template, data) else: return JsonResponse(self.write_success(data)) def write_fail(self, code, message): response = { 'error': code, 'data': None, 'message': message } return response def write_success(self, data, message=""): response = { 'error': 0, 'data': data, 'message': message, } return response def write_response(self, data, message): """ 需要自定义message的使用这个函数, 只允许在render_{method}方法中调用 """ return JsonResponse(self.write_success(data, message)) def write_404(self): return render(self.request, 'client/404.jinja.html') def start_num(self, page_size=settings.PAGE_SIZE): """ 获取 列表的start_num 为了兼容客户端 """ # TODO Deprecated 过几个版本之后可以去掉,等app都改过来 start_num = int(self.request.GET.get('start_num', 0)) page = int(self.request.GET.get('page', 0)) if page > 0: start_num = (page - 1) * page_size return start_num def make_pair(self, data): val = data.pop('value', '') key = data.pop('key', '') if key: key += '__contains' if key: data.update({key: val}) for key in list(data.keys()): if data.get(key) == '': del data[key] return data def handle_filter(self, filter): if not isinstance(filter, str): return {} filter_data = json.loads(filter) return self.make_pair(filter_data) class ClientDefaultArgs(LazyAttrDict): """ 客户端每个接口默认传递的参数 """ args_CLIENT = { 'app_name': {}, # gengmei_doctor 'version': {}, # 应用版本 'channel': {'default': None}, # 去掉,eg: benzhan / AppStore 'lng': {'access': float, 'default': 0}, # 经度 'lat': {'access': float, 'default': 0}, # 纬度 'platform': {}, # choices in [android / iPhone] 'os_version': {'default': None}, # 系统版本 'model': {'default': None}, # 移动设备型号 'screen': {'default': None}, # 屏幕分辨率 'device_id': {'default': None}, # 设备唯一标识 'class_name': {'default': None}, # 仅Android:Application类名 'idfa': {'default': None}, # 仅iOS:广告唯一标识 'idfv': {'default': None}, # 仅iOS,iOS10之后idfa可以关掉 } def __init__(self, query_dict): super(ClientDefaultArgs, self).__init__(query_dict, self.args_CLIENT, None) @property def is_android(self): return self.platform == PLATFORM_CHOICES.ANDROID def get_min_verion(self, versions): """ 获取兼容到最小版本的版本号 """ version = None versions.sort() for v in versions: if LooseVersion(self.version) < LooseVersion(v): version = v break return version def get_device_id(self): device_id = self.device_id or self.idfv or self.idfa return device_id def get_offset_count(request): try: page = int(request.GET.get('page', 1)) except: page = 1 try: count = int(request.GET.get('count', 10)) except: count = 10 offset = count * (page-1) return offset, count