class FilterSchema(Schema): """Structure for a filter.""" function = fields.Str( required=False, missing="and", validate=validate.OneOf(["and", "or", "xor", "one"]), ) invert = fields.Boolean(required=False, missing=False) rules = fields.List(fields.Nested(RuleSchema), required=True, validate=validate.Length(min=1))
class ChangePasswordView(MethodView): """View to change the user password.""" decorators = [login_required] post_args = { 'password': fields.String(required=True, validate=[validate.Length(min=6, max=128)]), 'new_password': fields.String(required=True, validate=[validate.Length(min=6, max=128)]), } def verify_password(self, password=None, new_password=None, **kwargs): """Verify password is not invalid.""" if not verify_and_update_password(password, current_user): _abort(get_message('INVALID_PASSWORD')[0], 'password') if password == new_password: _abort(get_message('PASSWORD_IS_THE_SAME')[0], 'password') def change_password(self, new_password=None, **kwargs): """Perform any change password actions.""" after_this_request(_commit) change_user_password(user=current_user._get_current_object(), password=new_password) def success_response(self): """Return a successful change password response.""" return jsonify({'message': get_message('PASSWORD_CHANGE')[0]}) @use_kwargs(post_args) def post(self, **kwargs): """Change user password.""" if not current_security.changeable: _abort(get_message("PASSWORD_CHANGE_DISABLED")[0]) self.verify_password(**kwargs) self.change_password(**kwargs) return self.success_response()
class SuperShopVerifyView(UserBaseView): """总后台-修改店铺认证状态""" @use_args( { "sign": fields.String(required=True, comment="加密认证"), "timestamp": fields.Integer(required=True, comment="时间戳"), "user_id": fields.Integer(required=True, comment="用户ID"), "shop_id": fields.Integer( required=True, validate=[validate.Range(1)], comment="店铺ID"), "verify_status": fields.Integer( required=True, validate=[ validate.OneOf([ ShopVerifyActive.YES, ShopVerifyActive.CHECKING, ShopVerifyActive.REJECTED, ]) ], comment="店铺认证状态", ), "verify_type": fields.Integer( required=True, validate=[ validate.OneOf( [ShopVerifyType.ENTERPRISE, ShopVerifyType.INDIVIDUAL]) ], comment="店铺认证类型,个人/企业", ), "verify_content": fields.String(required=True, validate=[validate.Length(0, 200)], comment="认证内容"), }, location="json") @SuperBaseView.validate_sign("sign", ("user_id", "timestamp")) def put(self, request, args): shop = get_shop_by_shop_id(args.pop("shop_id")) if not shop: return self.send_fail(error_text="店铺不存在") serializer = SuperShopVerifySerializer(shop, data=args) if not serializer.is_valid(): return self.send_error(error_message=serializer.errors, status_code=status.HTTP_400_BAD_REQUEST) serializer.save() return self.send_success()
class MallBrowseRecord(MallBaseView): """商城-创建浏览记录""" @register_browse_record("product") def gen_product_browse_record(self, args: dict): product_id = int(args["spa_params"]["product_id"]) product_ids = list_product_ids_by_shop_id_interface( self.current_shop.id, [ProductStatus.ON, ProductStatus.OFF] ) if product_id not in product_ids: return False, "货品不存在" info = { "shop_id": self.current_shop.id, "user_id": self.current_user.id, "product_id": product_id, "start_time": args["start_time"], "duration": args["duration"], "pre_page_name": args["pre_page"].get("name"), "next_page_name": args["next_page"].get("name"), } create_product_browse_record(info) return True, None @use_args( { "fullpath": fields.String( required=True, validate=[validate.Length(1, 256)], comment="url全路径" ), "query": StrToDict(required=True, comment="路由里面的query参数"), "cur_page": StrToDict(required=True, comment="当前页面, 包含type, name2个值, str"), "pre_page": StrToDict(required=True, comment="上一个页面, 包含type, name2个值, str"), "next_page": StrToDict(required=True, comment="下一个页面, 包含type, name2个值, str"), "spa_query": StrToDict(required=True, comment="当前页面的一些参数"), "spa_params": StrToDict(required=True, comment="当前页面的一些参数"), "start_time": fields.DateTime(required=True, comment="进入当前页面的时间"), "duration": fields.Integer( required=True, validate=[validate.Range(0)], comment="在页面停留的时间" ), }, location="json" ) def post(self, request, args, shop_code): self._set_current_shop(request, shop_code) # 暂时只记录商品的访问记录, 以后再扩展 cur_page_type = args["cur_page"]["type"] gen_browse_record_func = _MAP_BROWSE_RECORD[cur_page_type] success, info = gen_browse_record_func(self, args) if not success: return self.send_fail(error_text=info) return self.send_success()
class RHUserSearch(RHProtected): """Search for users based on given criteria""" @use_args({ 'email': fields.Str(validate=lambda s: len(s) > 3 and '@' in s), 'name': fields.Str(validate=validate.Length(min=3)), 'favorites_first': fields.Bool(missing=False) }) def _process(self, args): if not args: raise BadRequest() favorites_first = args.pop('favorites_first') query = build_user_search_query(args, favorites_first=favorites_first) return jsonify(user_schema.dump(query.limit(10).all(), many=True))
class AdminProductGroupsView(AdminBaseView): """后台-货品-批量更新货品分组""" @AdminBaseView.permission_required([AdminBaseView.staff_permissions.ADMIN_PRODUCT]) @use_args( { "product_ids": fields.List( fields.Integer(required=True), required=True, validate=[validate.Length(1)], comment="货品ID列表", ), "group_id": fields.Integer(required=True, comment="货品分组ID"), }, location="json" ) def put(self, request, args): shop = self.current_shop group_id = args.get("group_id") product_ids = args.pop("product_ids") # 校验分组是否存在 product_group = get_product_group_by_shop_id_and_id(shop.id, group_id) if not product_group: return self.send_fail(error_text="货品分组不存在") # 获取货品,更新货品信息 update_product_product_group_by_ids(product_ids, group_id) return self.send_success() @AdminBaseView.permission_required([AdminBaseView.staff_permissions.ADMIN_PRODUCT]) @use_args( { "status": StrToList( required=False, missing=[ProductStatus.ON, ProductStatus.OFF], validate=[ validate.ContainsOnly( [ProductStatus.ON, ProductStatus.OFF] ) ], comment="货品状态,上架/下架", ) }, location="query" ) def get(self, request, args): shop = self.current_shop product_group_with_count = list_product_group_with_product_count(shop.id, **args) serializer = AdminProductGroupSerializer(product_group_with_count, many=True) return self.send_success(data_list=serializer.data)
class AdminConfigCustomShareDescriptionView(AdminBaseView): """后台-修改店铺分享信息中的自定义分享描述""" @AdminBaseView.permission_required( [AdminBaseView.staff_permissions.ADMIN_CONFIG]) @use_args( { "custom_share_description": fields.String(required=True, validate=[validate.Length(1, 45)], comment="自定义分享描述") }, location="json") def put(self, request, args): shop = self.current_shop update_share_setup(shop.id, args) return self.send_success()
class AdminConfigReceiptBottomQrcodeView(AdminBaseView): """后台-设置-小票设置-小票底部二维码设置""" @AdminBaseView.permission_required( [AdminBaseView.staff_permissions.ADMIN_CONFIG]) @use_args( { "bottom_qrcode": fields.String(required=True, validate=[validate.Length(0, 128)], comment="小票底部二维码") }, location="json") def put(self, request, args): shop_id = self.current_shop.id update_receipt_by_shop_id(shop_id, args) return self.send_success()
class RegisterResource(Resource): @use_args({ 'username': fields.Str(required=True, location='form'), 'password': fields.Str(required=True, location='form', validate=validate.Length(min=1)), 'format': fields.Str(missing='json', location='query', validate=validate.Equal('json')) }) def post(self, args): user = User(username=args['username'], password=args['password']) db.session.add(user) try: db.session.commit() except IntegrityError: db.session.rollback() return custom_error('username', ['Username already in use']), ErrorCode.BAD_REQUEST return {'message': 'OK'}
class AdminConfigShopImgView(AdminBaseView): """后台-设置-店铺信息-修改店铺logo""" @AdminBaseView.permission_required( [AdminBaseView.staff_permissions.ADMIN_CONFIG]) @use_args( { "shop_img": fields.String(required=True, validate=[validate.Length(1, 128)], comment="店铺logo") }, location="json", ) def put(self, request, args): shop = self.current_shop update_shop_data_interface(shop, args) return self.send_success()
class RHCreateBooking(RHRoomBookingBase): def _validate_room_booking_limit(self, start_dt, end_dt, booking_limit_days): day_start_dt = datetime.combine(start_dt.date(), time()) day_end_dt = datetime.combine(end_dt.date(), time(23, 59)) selected_period_days = (day_end_dt - day_start_dt).days return selected_period_days <= booking_limit_days @use_args({ 'start_dt': fields.DateTime(required=True), 'end_dt': fields.DateTime(required=True), 'repeat_frequency': EnumField(RepeatFrequency, required=True), 'repeat_interval': fields.Int(missing=0), 'room_id': fields.Int(required=True), 'user_id': fields.Int(), 'booking_reason': fields.String(validate=validate.Length(min=3)), 'is_prebooking': fields.Bool(missing=False) }) def _process(self, args): room = Room.get_one(args.pop('room_id')) user_id = args.pop('user_id', None) booked_for = User.get_one(user_id) if user_id else session.user is_prebooking = args.pop('is_prebooking') # Check that the booking is not longer than allowed booking_limit_days = room.booking_limit_days or rb_settings.get( 'booking_limit') if not self._validate_room_booking_limit( args['start_dt'], args['end_dt'], booking_limit_days): msg = ( _('Bookings for the room "{}" may not be longer than {} days' ).format(room.name, booking_limit_days)) return jsonify(success=False, msg=msg) try: Reservation.create_from_data(room, dict(args, booked_for_user=booked_for), session.user, prebook=is_prebooking) db.session.flush() except NoReportError as e: db.session.rollback() return jsonify(success=False, msg=unicode(e)) return jsonify(success=True, is_prebooking=is_prebooking)
class ResetPasswordView(MethodView): """View to reset the user password.""" decorators = [user_already_authenticated] post_args = { "token": fields.String(required=True), "password": fields.String( required=True, validate=[validate.Length(min=6, max=128)] ), } def get_user(self, token=None, **kwargs): """Retrieve a user by the provided arguments.""" # Verify the token expired, invalid, user = reset_password_token_status(token) if invalid: _abort(get_message("INVALID_RESET_PASSWORD_TOKEN")[0]) if expired: _abort( get_message( "PASSWORD_RESET_EXPIRED", email=user.email, within=current_security.reset_password_within, )[0] ) return user def success_response(self, user): """Return a successful reset password response.""" return jsonify({"message": get_message("PASSWORD_RESET")[0]}) @use_kwargs(post_args) def post(self, **kwargs): """Reset user password.""" # TODO there doesn't seem to be a `.resettable` or similar? if not current_security.recoverable: _abort(get_message("PASSWORD_RESET_DISABLED")[0]) user = self.get_user(**kwargs) after_this_request(_commit) update_password(user, kwargs["password"]) login_user(user) return self.success_response(user)
class EnvironmentResource(Resource): @staticmethod def get(environment_id): with make_session() as session: data = session.query(Environment).filter( Environment.id == environment_id).first() # type: Environment if data is None: raise NotFound("Requested environment does not exist") return marshal(data[0], environment_fields) @staticmethod def delete(environment_id): with make_session() as session: data = session.query(Environment).filter( Environment.id == environment_id).first() # type: Environment if data is None: raise NotFound("Requested environment does not exist") if data.in_use(): raise Conflict("Requested environment is in use") session.delete(data) return make_empty_response() patch_args = { 'name': fields.String(required=True, validate=(validate.Length(min=1, max=255), validate.Regexp('[^/]+')), trim=True) } @staticmethod @use_kwargs(patch_args) def patch(environment_id, name): with make_session() as session: data = session.query(Environment).filter( Environment.id == environment_id).first() # type: Environment if data is None: raise NotFound("Requested environment does not exist") data.name = name return make_empty_response()
class Main(Resource): args = { "query": fields.Raw(required=True, validate=validate.Length(1, max=None)), "count": fields.Int(required=False, validate=validate.Range(1, max=None)) } def __init__(self, indexer: Indexer): self.__indexer = indexer @use_args(args) def get(self, args): queryResult = self.__indexer.query(args["query"]) if "count" in args: return queryResult.jsonify(count=args["count"]) else: return queryResult.jsonify()
class AdminDeliveryConfigPickView(AdminBaseView): """后台-订单-自提设置""" # 参数校验 def validate_time(self): try: datetime.datetime.strptime(self, "%H:%M") except Exception: return False return True @use_args( { "pick_service_amount": fields.Float(required=True, comment="自提模式服务费"), "pick_minimum_free_amount": fields.Float( required=True, comment="自提模式免服务费最小金额" ), "pick_today_on": fields.Boolean(required=True, comment="今天自提是否开启"), "pick_tomorrow_on": fields.Boolean(required=True, comment="明天自提是否开启"), "pick_periods": fields.Nested( { "from_time": fields.String( required=True, comment="自提起始时间", validate=validate_time ), "to_time": fields.String( required=True, comment="自提终止时间", validate=validate_time ), }, many=True, validate=[validate.Length(1)], unknown=True, comment="自提时段", ), }, location="json" ) def put(self, request, args): success, msg = update_delivery_config( self.current_shop.id, args, self.current_user.id ) if not success: return self.send_fail(error_text=msg) return self.send_success()
def _parse_review_args(event, review_type): args_schema = { 'proposed_action': EnumField(PaperAction, required=True), 'comment': fields.String(missing='') } for question in event.cfp.get_questions_for_review_type(review_type): attrs = {} if question.is_required: attrs['required'] = True else: attrs['missing'] = None if question.field_type == 'rating': field_cls = fields.Integer elif question.field_type == 'text': validators = [] if question.field_data['max_length']: validators.append( validate.Length(max=question.field_data['max_length'])) if question.field_data['max_words']: validators.append(max_words(question.field_data['max_words'])) attrs['validate'] = validators field_cls = fields.String elif question.field_type == 'bool': field_cls = fields.Bool else: raise Exception( f'Invalid question field type: {question.field_type}') args_schema[f'question_{question.id}'] = field_cls(**attrs) data = parser.parse(args_schema) questions_data = { k: v for k, v in data.items() if k.startswith('question_') } review_data = { k: v for k, v in data.items() if not k.startswith('question_') } return questions_data, review_data
class AdminProductGroupSortView(AdminBaseView): """后台-货品-分组排序""" @AdminBaseView.permission_required([AdminBaseView.staff_permissions.ADMIN_PRODUCT]) @use_args( { "group_ids": fields.List( fields.Integer(required=True), required=True, validate=[validate.Length(1)], comment="货品分组ID列表", ) }, location="json" ) def put(self, request, args): group_ids = args.get("group_ids") shop = self.current_shop sort_shop_product_group(shop.id, group_ids) return self.send_success()
class AdminCustomerRemarkView(AdminBaseView): """后台-客户-更改客户备注""" @AdminBaseView.permission_required( [AdminBaseView.staff_permissions.ADMIN_CUSTOMER]) @use_args( { "customer_id": fields.Integer(required=True, comment="客户ID"), "remark": fields.String(required=True, validate=[validate.Length(0, 20)]), }, location="json", ) def put(self, request, args): shop = self.current_shop customer = get_customer_by_customer_id_and_shop_id( args.get("customer_id"), shop.id) if not customer: return self.send_fail(error_text="客户或customer_id不存在") update_customer_remark(customer, args.get("remark")) return self.send_success()
class RelationResource(ProtectedResource, GrampsJSONEncoder): """Relation resource.""" @use_args( { "depth": fields.Integer(missing=15, validate=validate.Range(min=2)), "locale": fields.Str(missing=None, validate=validate.Length(min=1, max=5)), }, location="query", ) def get(self, args: Dict, handle1: Handle, handle2: Handle) -> Response: """Get the most direct relationship between two people.""" db_handle = get_db_handle() person1 = get_person_by_handle(db_handle, handle1) if person1 == {}: abort(404) person2 = get_person_by_handle(db_handle, handle2) if person2 == {}: abort(404) locale = get_locale_for_language(args["locale"], default=True) calc = get_relationship_calculator(reinit=True, clocale=locale) calc.set_depth(args["depth"]) data = calc.get_one_relationship(db_handle, person1, person2, extra_info=True, olocale=locale) return self.response( 200, { "relationship_string": data[0], "distance_common_origin": data[1], "distance_common_other": data[2], }, )
class LoginView(MethodView, UserViewMixin): """View to login a user.""" decorators = [user_already_authenticated] post_args = { 'email': fields.Email(required=True, validate=[user_exists]), 'password': fields.String(required=True, validate=[validate.Length(min=6, max=128)]) } def success_response(self, user): """Return a successful login response.""" return jsonify(default_user_payload(user)) def verify_login(self, user, password=None, **kwargs): """Verify the login via password.""" if not user.password: _abort(get_message('PASSWORD_NOT_SET')[0], 'password') if not verify_and_update_password(password, user): _abort(get_message('INVALID_PASSWORD')[0], 'password') if requires_confirmation(user): _abort(get_message('CONFIRMATION_REQUIRED')[0]) if not user.is_active: _abort(get_message('DISABLED_ACCOUNT')[0]) def login_user(self, user): """Perform any login actions.""" return login_user(user) @use_kwargs(post_args) def post(self, **kwargs): """Verify and login a user.""" user = self.get_user(**kwargs) self.verify_login(user, **kwargs) self.login_user(user) return self.success_response(user)
class AdminConfigShopAddressView(AdminBaseView): """后台-设置-店铺信息-更改店铺地址""" @AdminBaseView.permission_required( [AdminBaseView.staff_permissions.ADMIN_CONFIG]) @use_args( { "shop_province": fields.Integer(required=True, comment="省份编号"), "shop_city": fields.Integer(required=True, comment="城市编号"), "shop_county": fields.Integer(required=True, comment="区份编号"), "shop_address": fields.String(required=True, validate=[validate.Length(0, 100)], comment="详细地址"), }, location="json") def put(self, request, args): shop = self.current_shop update_shop_data_interface(shop, args) return self.send_success()
class PackList(Resource): @staticmethod def get(): with make_session() as session: return marshal(session.query(Pack).all(), pack_fields) put_args = { 'name': fields.String(required=True, validate=(validate.Length(min=1, max=255), validate.Regexp('[^/]+')), trim=True) } @staticmethod @use_kwargs(put_args) def put(name): pack = Pack(name=name.strip()) with make_session() as session: session.add(pack) session.commit() return make_id_response(pack.id)
class LivingDatesResource(ProtectedResource, GrampsJSONEncoder): """Living calculator dates resource.""" @use_args( { "average_generation_gap": fields.Integer(missing=None, validate=validate.Range(min=1)), "locale": fields.Str(missing=None, validate=validate.Length(min=1, max=5)), "max_age_probably_alive": fields.Integer(missing=None, validate=validate.Range(min=1)), "max_sibling_age_difference": fields.Integer(missing=None, validate=validate.Range(min=1)), }, location="query", ) def get(self, args: Dict, handle: Handle) -> Response: """Determine estimated birth and death dates.""" db_handle = get_db_handle() locale = get_locale_for_language(args["locale"], default=True) person = get_person_by_handle(db_handle, handle) if person == {}: abort(404) data = probably_alive_range( person, db_handle, max_sib_age_diff=args["max_sibling_age_difference"], max_age_prob_alive=args["max_age_probably_alive"], avg_generation_gap=args["average_generation_gap"], ) profile = { "birth": locale.date_displayer.display(data[0]), "death": locale.date_displayer.display(data[1]), "explain": data[2], "other": data[3], } return self.response(200, profile)
class ResetPasswordView(MethodView): """View to reset the user password.""" decorators = [user_already_authenticated] post_args = { 'token': fields.String(required=True), 'password': fields.String(required=True, validate=[validate.Length(min=6, max=128)]), } def get_user(self, token=None, **kwargs): """Retrieve a user by the provided arguments.""" # Verify the token expired, invalid, user = reset_password_token_status(token) if invalid: _abort(get_message('INVALID_RESET_PASSWORD_TOKEN')[0]) if expired: _abort( get_message('PASSWORD_RESET_EXPIRED', email=user.email, within=current_security.reset_password_within)[0]) return user def success_response(self, user): """Return a successful reset password response.""" return jsonify({'message': get_message('PASSWORD_RESET')[0]}) @use_kwargs(post_args) def post(self, **kwargs): """Reset user password.""" user = self.get_user(**kwargs) after_this_request(_commit) update_password(user, kwargs['password']) login_user(user) return self.success_response(user)
class RelationsResource(ProtectedResource, GrampsJSONEncoder): """Relations resource.""" @use_args( { "depth": fields.Integer(missing=15, validate=validate.Range(min=2)), "locale": fields.Str(missing=None, validate=validate.Length(min=1, max=5)), }, location="query", ) def get(self, args: Dict, handle1: Handle, handle2: Handle) -> Response: """Get all possible relationships between two people.""" db_handle = get_db_handle() person1 = get_person_by_handle(db_handle, handle1) if person1 == {}: abort(404) person2 = get_person_by_handle(db_handle, handle2) if person2 == {}: abort(404) locale = get_locale_for_language(args["locale"], default=True) calc = get_relationship_calculator(reinit=True, clocale=locale) calc.set_depth(args["depth"]) data = calc.get_all_relationships(db_handle, person1, person2) result = [] index = 0 while index < len(data[0]): result.append({ "relationship_string": data[0][index], "common_ancestors": data[1][index], }) index = index + 1 if result == []: result = [{}] return self.response(200, result)
class GrampsObjectsResource(GrampsObjectResourceHelper, Resource): """Resource for multiple objects.""" @use_args( { "gramps_id": fields.Str(validate=validate.Length(min=1)), "strip": fields.Str(validate=validate.Length(equal=0)), "keys": fields.DelimitedList(fields.Str(), missing=[]), "skipkeys": fields.DelimitedList(fields.Str(), missing=[]), "profile": fields.Str(validate=validate.Length(equal=0)), "extend": fields.DelimitedList(fields.Str(validate=validate.Length(min=1))), "filter": fields.Str(validate=validate.Length(min=1)), "rules": fields.Str(validate=validate.Length(min=1)), }, location="query", ) def get(self, args: Dict) -> Response: """Get all objects.""" if "gramps_id" in args: obj = self.get_object_from_gramps_id(args["gramps_id"]) if obj is None: abort(404) return self.response(200, [self.object_extend(obj, args)], args) query_method = self.db_handle.method( "get_%s_from_handle", self.gramps_class_name ) if "filter" in args or "rules" in args: handle_list = apply_filter(self.db_handle, args, self.gramps_class_name) return self.response( 200, [ self.object_extend(query_method(handle), args) for handle in handle_list ], args, ) iter_method = self.db_handle.method("iter_%s_handles", self.gramps_class_name) return self.response( 200, [ self.object_extend(query_method(handle), args) for handle in iter_method() ], args, )
class EnvironmentList(Resource): @staticmethod def get(): with make_session() as session: return marshal( session.query(Environment).all(), environment_fields) put_args = { 'name': fields.String(required=True, validate=(validate.Length(min=1, max=255), validate.Regexp('[^/]+')), trim=True) } @staticmethod @use_kwargs(put_args) def put(name: str): environment = Environment(name=name.strip()) with make_session() as session: session.add(environment) session.commit() return make_id_response(environment.id)
class MallCartVerifyView(MallBaseView): """商城端-购物篮确认时检验""" @use_args( { "product_ids": fields.List( fields.Integer(required=True), required=True, validate=[validate.Length(1)], comment="商品ID", ) }, location="json") def post(self, request, args, shop_code): self._set_current_shop(request, shop_code) shop = self.current_shop product_list = list_product_by_ids_interface(shop.id, **args) for product in product_list: if product.status == ProductStatus.OFF: return self.send_fail( error_text="商品{product_name}已下架, 看看别的商品吧".format( product_name=product.name)) return self.send_success()
class AdminProductAliveGrouponView(AdminBaseView): """后台-货品-查询此刻或者未来有拼团活动的货品""" @AdminBaseView.permission_required([AdminBaseView.staff_permissions.ADMIN_PRODUCT]) @use_args( { "product_ids": fields.List( fields.Integer(required=True), required=True, validate=[validate.Length(1)], commet="货品ID列表", ) }, location="query", ) def get(self, request, args): product_ids = args["product_ids"] groupon_product_set = list_alive_groupon_by_product_ids_interface( product_ids ) product_ids_dict = {"alive_grouon_product": []} for product_id in groupon_product_set: product_ids_dict["alive_grouon_product"].append(product_id) return self.send_success(data=product_ids_dict)
class QueryArgs(Schema): fields = fields.DelimitedList(fields.Str(validate=validate.Length(min=1))) type = mfields.Str(required=True, validate=lambda v: v in CATEGORY)