interact.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. import tornado.websocket
  2. import time, json
  3. from collections import defaultdict
  4. from .framework import WebIOFuture, Msg, Global
  5. from collections.abc import Iterable, Mapping, Sequence
  6. import logging
  7. logger = logging.getLogger(__name__)
  8. def run_async(coro):
  9. Global.active_ws.inactive_coro_instances.append(coro)
  10. def send_msg(cmd, spec=None):
  11. msg = dict(command=cmd, spec=spec, coro_id=Global.active_coro_id)
  12. Global.active_ws.write_message(json.dumps(msg))
  13. def get_response(cmd, spec):
  14. send_msg(cmd, spec)
  15. response_msg = yield from WebIOFuture()
  16. return response_msg
  17. async def next_event():
  18. res = await WebIOFuture()
  19. return res
  20. TEXT = 'text'
  21. NUMBER = "number"
  22. PASSWORD = "password"
  23. CHECKBOX = 'checkbox'
  24. RADIO = 'radio'
  25. SELECT = 'select'
  26. async def _input_event_handle(valid_funcs, whole_valid_func, inputs_args):
  27. """
  28. 根据提供的校验函数处理表单事件
  29. :param valid_funcs: map(name -> valid_func) valid_func 为 None 时,不进行验证
  30. valid_func: callback(data) -> error_msg
  31. :param whole_valid_func: callback(data) -> (name, error_msg)
  32. :param inputs_args:
  33. :return:
  34. """
  35. while True:
  36. event = await next_event()
  37. event_name, event_data = event['event'], event['data']
  38. if event_name == 'input_event':
  39. input_event = event_data['event_name']
  40. if input_event == 'blur':
  41. onblur_name = event_data['name']
  42. valid_func = valid_funcs.get(onblur_name)
  43. if valid_func is None:
  44. continue
  45. val = _pre_covert_res(event_data['value'], inputs_args[onblur_name])
  46. error_msg = valid_func(val)
  47. if error_msg is not None:
  48. send_msg('update_input', dict(target_name=onblur_name, attributes={
  49. 'valid_status': False,
  50. 'invalid_feedback': error_msg
  51. }))
  52. elif event_name == 'from_submit':
  53. all_valid = True
  54. # 调用输入项验证函数进行校验
  55. for name, valid_func in valid_funcs.items():
  56. if valid_func is None:
  57. continue
  58. val = _pre_covert_res(event_data[name], inputs_args[name])
  59. error_msg = valid_func(val)
  60. if error_msg is not None:
  61. all_valid = False
  62. send_msg('update_input', dict(target_name=name, attributes={
  63. 'valid_status': False,
  64. 'invalid_feedback': error_msg
  65. }))
  66. # 调用表单验证函数进行校验
  67. if whole_valid_func:
  68. data = {k: _pre_covert_res(v, inputs_args[k]) for k, v in event_data.items()}
  69. v_res = whole_valid_func(data)
  70. if v_res is not None:
  71. all_valid = False
  72. onblur_name, error_msg = v_res
  73. send_msg('update_input', dict(target_name=onblur_name, attributes={
  74. 'valid_status': False,
  75. 'invalid_feedback': error_msg
  76. }))
  77. if all_valid:
  78. break
  79. return event['data']
  80. def _make_input_spec(label, type, name, valid_func=None, multiple=None, inline=None, other_html_attrs=None,
  81. **other_kwargs):
  82. """
  83. 校验传入input函数和select函数的参数
  84. 生成input_group消息中spec inputs参数列表项
  85. 支持的input类型 TEXT, NUMBER, PASSWORD, CHECKBOX, RADIO, SELECT
  86. """
  87. allowed_type = {TEXT, NUMBER, PASSWORD, CHECKBOX, RADIO, SELECT}
  88. assert type in allowed_type, 'Input type not allowed.'
  89. input_item = other_kwargs
  90. input_item.update(other_html_attrs or {})
  91. input_item.update(dict(label=label, type=type, name=name))
  92. if valid_func is not None and type in (CHECKBOX, RADIO): # CHECKBOX, RADIO 不支持valid_func参数
  93. logger.warning('valid_func can\'t be used when type in (CHECKBOX, RADIO)')
  94. if inline is not None and type not in {CHECKBOX, RADIO}:
  95. logger.warning('inline 只能用于 CHECKBOX, RADIO type, now type:%s', type)
  96. elif inline is not None:
  97. input_item['inline'] = inline
  98. if multiple is not None and type != SELECT:
  99. logger.warning('multiple 参数只能用于SELECT type, now type:%s', type)
  100. elif multiple is not None:
  101. input_item['multiple'] = multiple
  102. if type in {CHECKBOX, RADIO, SELECT}:
  103. assert 'options' in input_item, 'Input type not allowed.'
  104. assert isinstance(input_item['options'], Iterable), 'options must be list type'
  105. # option 可用形式:
  106. # {value:, label:, [selected:,] [disabled:]}
  107. # (value, label, [selected,] [disabled])
  108. # value 单值,label等于value
  109. opts = input_item['options']
  110. opts_res = []
  111. for opt in opts:
  112. if isinstance(opt, Mapping):
  113. assert 'value' in opt and 'label' in opt, 'options item must have value and label key'
  114. elif isinstance(opt, list):
  115. assert len(opt) > 1 and len(opt) <= 4, 'options item format error'
  116. opt = dict(zip(('value', 'label', 'selected', 'disabled'), opt))
  117. else:
  118. opt = dict(value=opt, label=opt)
  119. opts_res.append(opt)
  120. input_item['options'] = opts_res
  121. # todo spec参数中,为None的表示使用默认,不发送
  122. for attr, val in list(input_item.items()):
  123. if val is None:
  124. del input_item[attr]
  125. return input_item
  126. def _pre_covert_res(value, kwargs):
  127. """对接收到的数据预处理"""
  128. if kwargs.get('type') == NUMBER:
  129. return int(value)
  130. return value
  131. async def input(label, type=TEXT, *, valid_func=None, name='data', value='', placeholder='', required=None,
  132. readonly=None,
  133. disabled=None, **other_html_attrs):
  134. input_kwargs = dict(locals())
  135. input_kwargs['label'] = ''
  136. input_kwargs['__name__'] = input.__name__
  137. input_kwargs.setdefault('autofocus', True) # 如果没有设置autofocus参数,则开启参数
  138. # 参数检查
  139. allowed_type = {TEXT, NUMBER, PASSWORD}
  140. assert type in allowed_type, 'Input type not allowed.'
  141. data = await input_group(label=label, inputs=[input_kwargs])
  142. return data[name]
  143. async def select(label, options, type=SELECT, *, multiple=None, valid_func=None, name='data', value='', placeholder='',
  144. required=None, readonly=None, disabled=None, inline=None, **other_html_attrs):
  145. """
  146. 参数值为None表示不指定,使用默认值
  147. :param label:
  148. :param options: option 列表
  149. option 可用形式:
  150. {value:, label:, [selected:,] [disabled:]}
  151. (value, label, [selected,] [disabled])
  152. value 单值,label等于value
  153. :param type:
  154. :param multiple:
  155. :param valid_func:
  156. :param name:
  157. :param value:
  158. :param placeholder:
  159. :param required:
  160. :param readonly:
  161. :param disabled:
  162. :param inline:
  163. :param other_html_attrs:
  164. :return:
  165. """
  166. input_kwargs = dict(locals())
  167. input_kwargs['label'] = ''
  168. input_kwargs['__name__'] = select.__name__
  169. if type == SELECT:
  170. input_kwargs.setdefault('autofocus', True) # 如果没有设置autofocus参数,则开启参数
  171. allowed_type = {CHECKBOX, RADIO, SELECT}
  172. assert type in allowed_type, 'Input type not allowed.'
  173. data = await input_group(label=label, inputs=[input_kwargs])
  174. return data[name]
  175. def _make_actions_input_spec(label, buttons, name):
  176. """
  177. :param label:
  178. :param actions: action 列表
  179. action 可用形式:
  180. {value:, label:, [disabled:]}
  181. (value, label, [disabled])
  182. value 单值,label等于value
  183. :return:
  184. """
  185. act_res = []
  186. for act in buttons:
  187. if isinstance(act, Mapping):
  188. assert 'value' in act and 'label' in act, 'actions item must have value and label key'
  189. elif isinstance(act, list):
  190. assert len(act) in (2, 3), 'actions item format error'
  191. act = dict(zip(('value', 'label', 'disabled'), act))
  192. else:
  193. act = dict(value=act, label=act)
  194. act_res.append(act)
  195. input_item = dict(type='actions', label=label, name=name, buttons=act_res)
  196. return input_item
  197. async def actions(label, buttons, name='data'):
  198. """
  199. 选择一个动作。UI为多个按钮,点击后会将整个表单提交
  200. :param label:
  201. :param actions: action 列表
  202. action 可用形式:
  203. {value:, label:, [disabled:]}
  204. (value, label, [disabled])
  205. value 单值,label等于value
  206. 实现方式:
  207. 多个type=submit的input组成
  208. [
  209. <button data-name value>label</button>,
  210. ...
  211. ]
  212. """
  213. input_kwargs = dict(label='', buttons=buttons, name=name)
  214. input_kwargs['__name__'] = actions.__name__
  215. data = await input_group(label=label, inputs=[input_kwargs])
  216. return data[name]
  217. async def input_group(label, inputs, valid_func=None):
  218. """
  219. :param label:
  220. :param inputs: list of generator or dict, dict的话,需要多加一项 __name__ 为当前函数名
  221. :param valid_func: callback(data) -> (name, error_msg)
  222. :return:
  223. """
  224. make_spec_funcs = {
  225. actions.__name__: _make_actions_input_spec,
  226. input.__name__: _make_input_spec,
  227. select.__name__: _make_input_spec,
  228. }
  229. item_valid_funcs = {}
  230. inputs_args = {}
  231. spec_inputs = []
  232. for input_g in inputs:
  233. if isinstance(input_g, dict):
  234. func_name = input_g.pop('__name__')
  235. input_kwargs = input_g
  236. else:
  237. input_kwargs = dict(input_g.cr_frame.f_locals) # 拷贝一份,不可以对locals进行修改
  238. func_name = input_g.__name__
  239. input_name = input_kwargs['name']
  240. inputs_args[input_name] = input_kwargs
  241. item_valid_funcs[input_name] = input_kwargs.get('valid_func')
  242. input_item = make_spec_funcs[func_name](**input_kwargs)
  243. spec_inputs.append(input_item)
  244. if all('autofocus' not in i for i in spec_inputs): # 每一个输入项都没有设置autofocus参数
  245. for i in spec_inputs:
  246. text_inputs = {TEXT, NUMBER, PASSWORD, SELECT} # todo update
  247. if i.get('type') in text_inputs:
  248. i['autofocus'] = True
  249. break
  250. send_msg('input_group', dict(label=label, inputs=spec_inputs))
  251. data = await _input_event_handle(item_valid_funcs, valid_func, inputs_args)
  252. send_msg('destroy_form')
  253. return data
  254. def set_title(title):
  255. send_msg('output_ctl', dict(title=title))
  256. def text_print(text, *, ws=None):
  257. msg = dict(command="output", spec=dict(content=text, type='text'))
  258. (ws or Global.active_ws).write_message(json.dumps(msg))
  259. def json_print(obj):
  260. text = "```\n%s\n```" % json.dumps(obj, indent=4, ensure_ascii=False)
  261. text_print(text)
  262. put_markdown = text_print