def manage_func(genericAPIView, user, app, model, func_name, params): """云函数, 由客户端直接调用的服务函数""" # import ipdb; ipdb.set_trace() func, options = find_func(app, model, func_name) if not func: raise exceptions.BusinessException( error_code=exceptions.FUNCTION_NOT_FOUNT, error_data=f"no such func: {func_name} found", ) if options.get("login_required", False): if not user.is_authenticated: raise PermissionDenied() if options.get("staff_required", False): if not user.is_staff: raise PermissionDenied() if options.get("superuser_required", False): if not user.is_superuser: raise PermissionDenied() view_context = {"view": genericAPIView} params["view_context"] = view_context result = func(user, **params) # TODO:考虑函数的返回结果类型。1. 实体,2.实体列表,3.字典,4.无返回,针对不同的结果给客户端反馈 if isinstance(result, requests.Response): response = HttpResponse(result, result.headers.get("Content-Type", None)) if "Content-disposition" in result.headers: response["Content-disposition"] = result.headers.get( "Content-disposition") return response if (isinstance(result, list) or isinstance(result, dict) or isinstance(result, str) or isinstance(result, bytes)): return success_response(result) return success_response()
def client_func(genericAPIView, user, app, model, func_name, params): """云函数, 由客户端直接调用的服务函数""" func, options = find_func(app, model, func_name) if not func: raise exceptions.BusinessException( error_code=exceptions.FUNCTION_NOT_FOUNT, error_data=f"no such func: {func_name} found", ) if options.get("login_required", False): if not user.is_authenticated: raise PermissionDenied() view_context = {"view": genericAPIView} params["view_context"] = view_context result = func(user, **params) # TODO:考虑函数的返回结果类型。1. 实体,2.实体列表,3.字典,4.无返回,针对不同的结果给客户端反馈 if isinstance(result, requests.Response): return HttpResponse(result, result.headers.get("Content-Type", None)) if isinstance(result, (list, dict)): return success_response(result) if isinstance(result, genericAPIView.model): serializer = genericAPIView.get_serializer(result) return success_response(serializer.data) return success_response()
def basebone_get_statistics_config(self): """ FIXME: 获取统计配置,暂时只支持管理端,客户端待完成 """ if self.end_slug == const.MANAGE_END_SLUG: # 如果是管理端的配置,直接使用 admin 中的配置 admin_class = self.get_bsm_model_admin() if admin_class: config = getattr(admin_class, admin.BSM_STATISTICS, None) if not config: raise exceptions.BusinessException( error_code=exceptions.BSM_NOT_STATISTICS_CONFIG ) return config raise exceptions.BusinessException( error_code=exceptions.BSM_CAN_NOT_FIND_ADMIN )
def validate_call_api_permission(self, request): """校验是否具有调用指定接口的权限 例如管理人员调用商品的列表接口,如果开启了使用接口校验,则需要校验此 用户是否具有调用此接口的权限,如果没有对应的权限,则就抛出没有权限 注意:如果是云函数,则另做处理 """ if not settings.MANAGE_API_PERMISSION_VALIDATE_ENABLE: return # if self.action == 'func': # func_name = request.data.get('func_name', None) or request.GET.get( # 'func_name', None # ) # perm_list = [f'basebone_api_model_func_{func_name}'] # else: # perm_map = { # 'create': ['create'], # 'list': ['list'], # 'set': ['set'], # 'destroy': ['retrieve', 'destroy'], # 'update': ['retrieve', 'update'], # 'partial_update': ['retrieve', 'partial_update'], # 'custom_patch': ['retrieve', 'custom_patch'], # 'update_sort': ['retrieve', 'update_sort'], # 'batch': ['retrieve', 'batch'], # 'export_file': ['export_file'], # } # perm_list = [ # f'basebone_api_model_{item}' for item in perm_map.get(self.action) # ] # check = request.user.has_perms(perm_list) # if not check: # raise exceptions.BusinessException(error_code=exceptions.REQUEST_FORBIDDEN) # TODO: 暂时提供测试而已,后面会改掉 perm_map = { "retrieve": ["view"], "create": ["add"], "list": ["view"], "update": ["view", "change"], "partial_update": ["view", "change"], "destroy": ["view", "delete"], } perm_list = [ f"{item}_{self.model_slug}" for item in perm_map.get(self.action, []) ] if perm_list: check = request.user.has_perms(perm_list) if not check: raise exceptions.BusinessException( error_code=exceptions.REQUEST_FORBIDDEN)
def perform_authentication(self, request): """ 截断,校验对应的 app 和 model 是否合法以及赋予当前对象对应的属性值 - 检验 app 和 model 是否合法 - 加载 admin 模块 - 记录模型对象 - 处理展开字段 - 处理树形数据 - 给数据自动插入用户数据 """ result = super().perform_authentication(request) self.app_label, self.model_slug = self.kwargs.get("app"), self.kwargs.get( "model" ) # 检测模型是否合法 try: self.model = apps.get_model(self.app_label, self.model_slug) except LookupError: raise exceptions.BusinessException( error_code=exceptions.MODEL_SLUG_IS_INVALID ) # 检测方法是否允许访问 model_str = f"{self.app_label}__{self.model_slug}" expose = exposed_apis.get(model_str, None) real_action = self.action if self.action == "set": real_action = "list" if ( not expose or not expose.get("actions", None) or real_action not in expose["actions"] ): raise exceptions.BusinessException( error_code=exceptions.THIS_ACTION_IS_NOT_AUTHENTICATE ) meta.load_custom_admin_module() self.get_expand_fields() self._get_data_with_tree(request) return result
def handle(self): request = self.context["request"] try: return self.bsm_batch_action( request, self.bsm_batch_queryset, self.data.get("payload", {}) ) except Exception as e: raise exceptions.BusinessException( error_code=exceptions.BATCH_ACTION_HAND_ERROR, error_data=str(e) )
def proxy(*args, **kwargs): signature = inspect.signature(func) required = { k for k, p in list(signature.parameters.items())[len(args):] if p.default == Parameter.empty and p.kind in [Parameter.KEYWORD_ONLY, Parameter.POSITIONAL_OR_KEYWORD] } lack = required - kwargs.keys() if lack: raise exceptions.BusinessException( exceptions.PARAMETER_FORMAT_ERROR, "、".join(lack) + " 必填") return func(*args, **kwargs)
def validate_data(self, value): """ - 校验 data - 记录当前批量的数据 """ model = self.context.get("view").model filter_params = {f"{model._meta.pk.name}__in": value} queryset = model.objects.filter(**filter_params) if len(value) != queryset.count(): raise exceptions.BusinessException( error_code=exceptions.PARAMETER_BUSINESS_ERROR, error_message="列表中包含不合法 id 的数据", ) self.bsm_batch_queryset = queryset return value
def validate_action(self, value): """ - 校验 action - 记录当前批量动作 """ view = self.context["view"] actions = get_model_batch_actions(view.model, end=view.end_slug) for key, val in common_action_map.items(): actions.setdefault(key, val) if value not in actions: raise exceptions.BusinessException( error_code=exceptions.PARAMETER_FORMAT_ERROR, error_data="传入的 action: {} 不支持".format(value), ) self.bsm_batch_action = actions[value] return value
def forward_one_to_many(field, value, update_data): """处理正向的一对多关系 - 如果数据不是一个字典,则直接返回 - 如果数据是字典,字典中没有包含主键,则对字典中的数据进行创建 - 如果数据是字典,字典中包含主键,则对主键指定的数据进行更新 """ if not isinstance(value, dict): return model, key = field.related_model, field.name pk_field_name = model._meta.pk.name # 如果传进来的数据不包含主键,则代表是创建数据 if pk_field_name not in value: # FIXME: 嵌套处理 forward_relation_hand(model, value) serializer = create_serializer_class(model, allow_one_to_one=True)(data=value) serializer.is_valid(raise_exception=True) instance = serializer.save() update_data[key] = instance.pk reverse_relation(model, value, instance) return update_data # 如果传进来的数据包含主键,则代表是更新数据 instance = model.objects.filter(**{pk_field_name: value[pk_field_name]}).first() if not instance: raise exceptions.BusinessException( error_code=exceptions.OBJECT_NOT_FOUND, error_data=f"{key}: {value} 指定的主键找不到对应的数据", ) # FIXME: 嵌套处理 forward_relation_hand(model, value) serializer = create_serializer_class(model, allow_one_to_one=True)( instance=instance, data=value, partial=True ) serializer.is_valid(raise_exception=True) serializer.save() update_data[key] = value[pk_field_name] reverse_relation(model, value, instance) return update_data
def reverse_one_to_one(field, value, instance): model = field.related_model pk_field = model._meta.pk if isinstance(value, dict): remote_field_name = field.remote_field.name Serializer = create_serializer_class(model, allow_one_to_one=True) value = forward_relation_hand(model, value) if pk_field.name not in value: model.objects.filter(**{field.remote_field.name: instance}).delete() value[field.remote_field.name] = instance.pk serializer = Serializer(data=value) else: pk_value = pk_field.to_python(value[pk_field.name]) filter_params = { pk_field.name: pk_value, field.remote_field.name: instance, } obj = model.objects.filter(**filter_params).first() if not obj: raise exceptions.BusinessException( error_code=exceptions.OBJECT_NOT_FOUND, error_data=f"{model}指定的主键[{pk_value}]找不到对应的数据", ) value[remote_field_name] = instance.pk serializer = Serializer(instance=obj, data=value, partial=True) serializer.is_valid(raise_exception=True) obj = serializer.save() reverse_relation(field.related_model, value, obj) else: if value is None: model.objects.filter(**{field.remote_field.name: instance.pk}).delete() else: model.objects.filter( **{field.field_name: pk_field.to_python(value)} ).update(**{field.remote_field.name: instance.pk})
def reverse_many_to_many(instance, field, data): """ 处理反向多对多关系的数据 Params: instance object 对象 field object 反向的字段 data list 反向的模型字典列表数据 """ # 反向关系的数据必须是数组 if not isinstance(data, list): raise exceptions.BusinessException( error_code=exceptions.PARAMETER_FORMAT_ERROR, error_data=f"{field.name}: {data} 只能是列表", ) # TODO: 如果使用了自定义的中间表,此种业务暂时不做处理 # TODO: 支持自定义中间表,如果非单纯中间表让它自行报异常,中间表必填值以后可以考虑使用 through_defaults # if field.through_fields: # return model = field.related_model related_name = meta.get_accessor_name(field) reverse_manager = getattr(instance, related_name, None) if related_name else None if not data: # 传入数据为空的场景 # 更新操作,如果传入空的数据,则清除掉此对象所有的数据 if reverse_manager: reverse_manager.clear() return # 传入数据不为空的情况下 pk_field_name = model._meta.pk.name # 迭代处理反向数据,这个时候还没有处理数据和对象的关系 reverse_object_list = set() for item_value in data: if isinstance(item_value, dict): if pk_field_name not in item_value: # 创建反向模型的数据 serializer = create_serializer_class(model)(data=item_value) serializer.is_valid(raise_exception=True) item_instance = serializer.save() else: # 更新反向模型的数据 item_instance = model.objects.filter( **{ pk_field_name: item_value[pk_field_name] }).first() if not item_instance: raise exceptions.BusinessException( error_code=exceptions.OBJECT_NOT_FOUND, error_data= f"{pk_field_name}: {item_value} 指定的主键找不到对应的数据", ) serializer = create_serializer_class(model)( instance=item_instance, data=item_value, partial=True) serializer.is_valid(raise_exception=True) serializer.save() reverse_object_list.add(item_instance.pk) else: reverse_object_list.add(item_value) # 处理数据和 instance 之间的关系 if reverse_manager: reverse_manager.set(list(reverse_object_list))
def export_file(self, request, *args, **kwargs): """输出 excel 和 excel 文件 ``` Params: fileformat string csv | excel 形式使用 querystring 的形式 ``` """ # 检测是否支持导出 admin_class = self.get_bsm_model_admin() if self._export_type_config.get("version") != "v2": if admin_class: exportable = getattr(admin_class, admin.BSM_EXPORTABLE, False) if not exportable: raise exceptions.BusinessException( error_code=exceptions.MODEL_EXPORT_IS_NOT_SUPPORT) else: raise exceptions.BusinessException( error_code=exceptions.MODEL_EXPORT_IS_NOT_SUPPORT) if not self._export_type_config: raise exceptions.BusinessException( error_code=exceptions.MODEL_EXPORT_IS_NOT_SUPPORT) csv_file, excel_file = "csv", "excel" valid_list = (csv_file, excel_file) file_type = self._export_type_config["file_type"] file_type = file_type if file_type in valid_list else csv_file queryset = self.filter_queryset(self.get_queryset()) export_version = self._export_type_config.get("version") if export_version == "v2": serializer_class = get_export_serializer_class( self.model, self.get_serializer_class(), version=export_version) return renderers_v2.csv_render( self.model, queryset, serializer_class, export_config=self._export_type_config, ) serializer_queryset_handler = self._export_type_config.get( "serializer_queryset_handler") if serializer_queryset_handler: func_handler = getattr(admin_class, serializer_queryset_handler, None) if func_handler: queryset = func_handler(queryset) actual_app_label = self._export_type_config.get("actual_app_label") actual_model_slug = self._export_type_config.get("actual_model_slug") if actual_app_label and actual_model_slug: self.model = apps.get_model(actual_app_label, actual_model_slug) custom_serializer_class = self._export_type_config.get( "serializer_class") serializer_class = get_export_serializer_class( self.model, self.get_serializer_class(), custom_serializer_class=custom_serializer_class, ) return renderers.csv_render( self.model, queryset, serializer_class, export_config=self._export_type_config, )
def check_app_model(self, request): # 是否对结果集进行去重 self.basebone_distinct_queryset = False # 模型角色配置 self.model_role_config = None self.app_label, self.model_slug = self.kwargs.get( "app"), self.kwargs.get("model") try: self.model = apps.get_model(self.app_label, self.model_slug) except LookupError: raise exceptions.BusinessException( error_code=exceptions.MODEL_SLUG_IS_INVALID) # 加载 admin 配置 meta.load_custom_admin_module() if self.action == "export_file": admin_class = self.get_bsm_model_admin() self._export_type_config = None if admin_class: exportable = getattr(admin_class, admin.BSM_EXPORTABLE, False) if exportable: exportable_map = {item["key"]: item for item in exportable} file_type = self.request.query_params.get("fileformat") if file_type in exportable_map: valid_item = exportable_map[file_type] else: valid_item = [ item for item in exportable if item["default"] ][0] self._export_type_config = valid_item if request.method.lower() == "get": if "basebone_export_config" in list( dict(request.query_params)): try: basebone_export_config = json.loads( request.query_params.get( "basebone_export_config")) except Exception: basebone_export_config = None self._export_type_config = basebone_export_config elif request.method.lower() == "post": if "basebone_export_config" in request.data: basebone_export_config = request.data.get( "basebone_export_config") if not isinstance(basebone_export_config, dict): basebone_export_config = None self._export_type_config = basebone_export_config if not self._export_type_config: raise exceptions.BusinessException( error_code=exceptions.MODEL_EXPORT_IS_NOT_SUPPORT) # FIXME: 如果不是新版导出,则重置 app_label 和 model_slug if self._export_type_config.get("version") != "v2": self.app_label = self._export_type_config["app_label"] self.model_slug = self._export_type_config[ "model_slug"] # 检测模型是否合法 try: self.model = apps.get_model( self.app_label, self.model_slug) except LookupError: raise exceptions.BusinessException( error_code=exceptions.MODEL_SLUG_IS_INVALID) else: if request.method.lower() == "get": if "basebone_export_config" in list( dict(request.query_params)): try: basebone_export_config = json.loads( request.query_params.get( "basebone_export_config")) except Exception: basebone_export_config = None self._export_type_config = basebone_export_config elif request.method.lower() == "post": if "basebone_export_config" in request.data: basebone_export_config = request.data.get( "basebone_export_config") if not isinstance(basebone_export_config, dict): basebone_export_config = None self._export_type_config = basebone_export_config if not self._export_type_config: raise exceptions.BusinessException( error_code=exceptions.MODEL_EXPORT_IS_NOT_SUPPORT) else: if request.method.lower() == "get": if "basebone_export_config" in list( dict(request.query_params)): try: basebone_export_config = json.loads( request.query_params.get( "basebone_export_config")) except Exception: basebone_export_config = None self._export_type_config = basebone_export_config elif request.method.lower() == "post": if "basebone_export_config" in request.data: basebone_export_config = request.data.get( "basebone_export_config") if not isinstance(basebone_export_config, dict): basebone_export_config = None self._export_type_config = basebone_export_config if not self._export_type_config: raise exceptions.BusinessException( error_code=exceptions.MODEL_EXPORT_IS_NOT_SUPPORT)
def forward_many_to_many(field, value, update_data): """处理正向的多对多关系 - 如果多对多关系的字段的值不是列表,则不做任何处理 - 如果传进来的值是一个列表,则不做任何处理 上面的后续校验扔给下一步的创建或者更新的表单验证 """ if not (isinstance(value, list) and value): return key = field.name pure_data, object_data = [], [] for item in value: object_data.append(item) if isinstance(item, dict) else pure_data.append(item) # 如果不包含对象数据,则不做任何处理 if not object_data: return model = field.related_model pk_field_name = model._meta.pk.name # 筛选更新的列表和创建的列表 create_list, update_list = [], [] for item in object_data: update_list.append(item) if pk_field_name in item else create_list.append(item) if create_list: create_ids = [] for item_data in create_list: # FIXME: 嵌套处理 forward_relation_hand(model, item_data) serializer = create_serializer_class(model, allow_one_to_one=True)( data=item_data ) serializer.is_valid(raise_exception=True) create_ids.append(serializer.save().pk) pure_data += create_ids if update_list: update_data_map = {item[pk_field_name]: item for item in update_list} filter_params = {f"{pk_field_name}__in": update_data_map.keys()} queryset = model.objects.filter(**filter_params) # 检查查询出的数据是否和传入的 id 长度一致 if queryset.count() != len(update_list): raise exceptions.BusinessException( error_code=exceptions.OBJECT_NOT_FOUND, error_data=f"{key}: {update_list} 存在不合法的数据", ) for instance in queryset.iterator(): item_data = update_data_map.get(getattr(instance, pk_field_name, None)) # FIXME: 嵌套处理 forward_relation_hand(model, item_data) serializer = create_serializer_class(model, allow_one_to_one=True)( instance=instance, data=item_data, partial=True ) serializer.is_valid(raise_exception=True) serializer.save() pure_data += update_data_map.keys() update_data[key] = pure_data return update_data
def reverse_one_to_many(field, value, instance, detail=True): """处理反向字段的多对一的数据 对于此种场景,数据格式是包含对象的列表或者已经存在对象的主键 注意事项: - 创建时,不能传入包含主键的数据 场景描述: class AModel(models.Model): pass class BModel(models.Model): a = models.ForgignKey(AModel) name = models.CharField() AModel 请求数据时,字段中包含 bmodel 对象如下数据格式: FIXME: 创建和更新时可以传入: bmodel: [ { name: 'xxxx' } ] 或者 FIXME: 只有更新时可以传入包含主键的数据: bmodel: [ { 'id': xxxx, 'name': 'update xxxxx' } ] """ model, key = field.related_model, field.name pk_field_name = model._meta.pk.name # 进行到这一步,说明对应的键时存在的,对于反向字段,就直接校验数据 if not isinstance(value, list): raise exceptions.BusinessException( error_code=exceptions.PARAMETER_FORMAT_ERROR, error_data=f"{key}: {value} 只能是列表", ) # 这里需要判断创造和更新 if detail: # 如果是更新,如果传入空的数据,则删除掉对应的数据 if not value: try: related_name = meta.get_relation_field_related_name( model, field.remote_field.name ) if related_name: relation = getattr(instance, related_name[0], None) if relation: relation.all().delete() return except Exception as e: raise exceptions.BusinessException( error_code=exceptions.PARAMETER_FORMAT_ERROR, error_data=str(e) ) else: # 如果是创建,如果传进来的值为空 if not value: return value = forward_relation_hand(model, value) pure_id_list, object_data_list = [], [] for item in value: if isinstance(item, dict): object_data_list.append(item) if pk_field_name in item: pure_id_list.append(item[pk_field_name]) else: pure_id_list.append(item) if object_data_list: # TODO: 创建时,不能传入包含主键的数据 for item in object_data_list: if not detail and pk_field_name in item: raise exceptions.BusinessException( error_code=exceptions.PARAMETER_BUSINESS_ERROR, error_data=f"{key}: {value} 当前为 create 操作,不能传入包含主键的数据", ) for item_value in object_data_list: if pk_field_name in item_value: # 此时说明是更新的数据 pk_value = model._meta.pk.to_python(item_value[pk_field_name]) filter_params = { pk_field_name: pk_value, } obj = model.objects.filter(**filter_params).first() if not obj: raise exceptions.BusinessException( error_code=exceptions.OBJECT_NOT_FOUND, error_data=f"{key}: {value} 指定的主键找不到对应的数据", ) item_value[field.remote_field.name] = instance.pk serializer = create_serializer_class(model, allow_one_to_one=True)( instance=obj, data=item_value, partial=True ) serializer.is_valid(raise_exception=True) serializer.save() else: # 如果传进来的数据不包含主键,则代表是创建数据 item_value[field.remote_field.name] = instance.pk serializer = create_serializer_class(model, allow_one_to_one=True)( data=item_value ) serializer.is_valid(raise_exception=True) obj = serializer.save() if detail: pure_id_list.append(getattr(obj, pk_field_name)) reverse_relation(field.related_model, item_value, obj) # 如果是更新,则删除掉对应的数据 if detail and pure_id_list: pure_id_list = [model._meta.pk.to_python(item) for item in pure_id_list] relation = meta.get_relation_field_related_name(model, field.remote_field.name) if relation: model.objects.filter(pk__in=pure_id_list).update( **{relation[1].name: instance} ) getattr(instance, relation[0]).exclude( **{f"{pk_field_name}__in": pure_id_list} ).delete() elif pure_id_list: # 如果是创建,则需要创建对应的数据 pure_id_list = [model._meta.pk.to_python(item) for item in pure_id_list] relation = meta.get_relation_field_related_name(model, field.remote_field.name) getattr(instance, relation[0]).add( *model.objects.filter(**{f"{pk_field_name}__in": pure_id_list}) )