1
0

interact.py 10 KB

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