input.py 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811
  1. """
  2. This module provides functions to get all kinds of input of user from the browser
  3. There are two ways to use the input functions, one is to call the input function alone to get a single input::
  4. name = input("What's your name")
  5. print("Your name is %s" % name)
  6. The other is to use `input_group` to get multiple inputs at once::
  7. info = input_group("User info",[
  8. input('Input your name', name='name'),
  9. input('Input your age', name='age', type=NUMBER)
  10. ])
  11. print(info['name'], info['age'])
  12. When use `input_group`, you needs to provide the ``name`` parameter in each input function to identify the input items in the result.
  13. .. note::
  14. PyWebIO determines whether the input function is in `input_group` or is called alone according to whether the
  15. ``name`` parameter is passed. So when calling an input function alone, **do not** set the ``name`` parameter;
  16. when calling the input function in `input_group`, you **must** provide the ``name`` parameter.
  17. By default, the user can submit empty input value. If the user must provide a non-empty input value, you need to
  18. pass ``required=True`` to the input function (some input functions do not support the ``required`` parameter)
  19. The input functions in this module is blocking, and the input form will be destroyed after successful submission.
  20. If you want the form to always be displayed on the page and receive input continuously,
  21. you can consider the :doc:`pin <./pin>` module.
  22. Functions list
  23. -----------------
  24. .. list-table::
  25. * - Function name
  26. - Description
  27. * - `input <pywebio.input.input>`
  28. - Text input
  29. * - `textarea <pywebio.input.textarea>`
  30. - Multi-line text input
  31. * - `select <pywebio.input.select>`
  32. - Drop-down selection
  33. * - `checkbox <pywebio.input.checkbox>`
  34. - Checkbox
  35. * - `radio <pywebio.input.radio>`
  36. - Radio
  37. * - `slider <pywebio.input.slider>`
  38. - Slider
  39. * - `actions <pywebio.input.actions>`
  40. - Actions selection
  41. * - `file_upload <pywebio.input.file_upload>`
  42. - File uploading
  43. * - `input_group <pywebio.input.input_group>`
  44. - Input group
  45. * - `input_update <pywebio.input.input_update>`
  46. - Update input item
  47. Functions doc
  48. --------------
  49. """
  50. import copy
  51. import logging
  52. import os.path
  53. from collections.abc import Mapping
  54. from typing import Any, Callable, Dict, List, Optional, Tuple, Union
  55. from .io_ctrl import input_control, output_register_callback, send_msg, single_input, single_input_kwargs
  56. from .platform import page as platform_setting
  57. from .session import get_current_session, get_current_task_id
  58. from .utils import Setter, check_dom_name_value, parse_file_size
  59. logger = logging.getLogger(__name__)
  60. TEXT = 'text'
  61. NUMBER = "number"
  62. FLOAT = "float"
  63. PASSWORD = "password"
  64. URL = "url"
  65. DATE = "date"
  66. TIME = "time"
  67. COLOR = "color"
  68. DATETIME_LOCAL = "datetime-local"
  69. DATETIME = DATETIME_LOCAL
  70. CHECKBOX = 'checkbox'
  71. RADIO = 'radio'
  72. SELECT = 'select'
  73. TEXTAREA = 'textarea'
  74. __all__ = ['TEXT', 'NUMBER', 'FLOAT', 'PASSWORD', 'URL', 'DATE',
  75. 'TIME', 'COLOR', 'DATETIME_LOCAL', 'DATETIME', 'input', 'textarea',
  76. 'select', 'checkbox', 'radio', 'actions', 'file_upload',
  77. 'slider', 'input_group', 'input_update']
  78. def _parse_args(kwargs, excludes=()):
  79. """parse the raw parameters that pass to input functions
  80. - excludes: the parameters that don't appear in returned spec
  81. - remove the parameters whose value is None
  82. :return:(spec,valid_func)
  83. """
  84. kwargs = {k: v for k, v in kwargs.items() if v is not None and k not in excludes}
  85. check_dom_name_value(kwargs.get('name', ''), '`name`')
  86. kwargs.update(kwargs.get('other_html_attrs', {}))
  87. kwargs.pop('other_html_attrs', None)
  88. if kwargs.get('validate'):
  89. kwargs['onblur'] = True
  90. valid_func = kwargs.pop('validate', lambda _: None)
  91. if kwargs.get('onchange'):
  92. onchange_func = kwargs['onchange']
  93. kwargs['onchange'] = True
  94. else:
  95. onchange_func = lambda _: None
  96. return kwargs, valid_func, onchange_func
  97. def input(label: str = '', type: str = TEXT, *, validate: Callable[[Any], Optional[str]] = None, name: str = None,
  98. value: str = None,
  99. action: Tuple[str, Callable[[Callable], None]] = None, onchange: Callable[[Any], None] = None,
  100. placeholder: str = None, required: bool = None,
  101. readonly: bool = None, datalist: List[str] = None, help_text: str = None, **other_html_attrs):
  102. r"""Text input
  103. :param str label: Label of input field.
  104. :param str type: Input type. Currently, supported types are:`TEXT` , `NUMBER` , `FLOAT` , `PASSWORD` , `URL` , `DATE` , `TIME`, `DATETIME`, `COLOR`
  105. The value of `DATE` , `TIME`, `DATETIME` type is a string in the format of `YYYY-MM-DD` , `HH:MM:SS` , `YYYY-MM-DDTHH:MM` respectively
  106. (`%Y-%m-%d`, `%H:%M:%S`, `%Y-%m-%dT%H:%M` in python `strptime() <https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior>`_ format).
  107. :param callable validate: Input value validation function. If provided, the validation function will be called when
  108. user completes the input field or submits the form.
  109. ``validate`` receives the input value as a parameter. When the input value is valid, it returns ``None``.
  110. When the input value is invalid, it returns an error message string. For example:
  111. .. exportable-codeblock::
  112. :name: input-valid-func
  113. :summary: `input()` validation
  114. def check_age(age):
  115. if age>30:
  116. return 'Too old'
  117. elif age<10:
  118. return 'Too young'
  119. input('Input your age', type=NUMBER, validate=check_age)
  120. :param str name: A string specifying a name for the input. Used with `input_group()` to identify different input
  121. items in the results of the input group. If call the input function alone, this parameter can **not** be set!
  122. :param str value: The initial value of the input
  123. :type action: tuple(label:str, callback:callable)
  124. :param action: Put a button on the right side of the input field, and user can click the button to set the value for the input.
  125. ``label`` is the label of the button, and ``callback`` is the callback function to set the input value when clicked.
  126. The callback is invoked with one argument, the ``set_value``. ``set_value`` is a callable object, which is
  127. invoked with one or two arguments. You can use ``set_value`` to set the value for the input.
  128. ``set_value`` can be invoked with one argument: ``set_value(value:str)``. The ``value`` parameter is the value to be set for the input.
  129. ``set_value`` can be invoked with two arguments: ``set_value(value:any, label:str)``. Each arguments are described as follows:
  130. * ``value`` : The real value of the input, can be any object. it will not be passed to the user browser.
  131. * ``label`` : The text displayed to the user
  132. When calling ``set_value`` with two arguments, the input item in web page will become read-only.
  133. The usage scenario of ``set_value(value:any, label:str)`` is: You need to dynamically generate the value of the
  134. input in the callback, and hope that the result displayed to the user is different from the actual submitted data
  135. (for example, result displayed to the user can be some user-friendly texts, and the value of the input can be
  136. objects that are easier to process)
  137. Usage example:
  138. .. exportable-codeblock::
  139. :name: input-action
  140. :summary: `input()` action usage
  141. import time
  142. def set_now_ts(set_value):
  143. set_value(int(time.time()))
  144. ts = input('Timestamp', type=NUMBER, action=('Now', set_now_ts))
  145. put_text('Timestamp:', ts) # ..demo-only
  146. ## ----
  147. from datetime import date,timedelta
  148. def select_date(set_value):
  149. with popup('Select Date'):
  150. put_buttons(['Today'], onclick=[lambda: set_value(date.today(), 'Today')])
  151. put_buttons(['Yesterday'], onclick=[lambda: set_value(date.today() - timedelta(days=1), 'Yesterday')])
  152. d = input('Date', action=('Select', select_date), readonly=True)
  153. put_text(type(d), d)
  154. Note: When using :ref:`Coroutine-based session <coroutine_based_session>` implementation, the ``callback``
  155. function can be a coroutine function.
  156. :param callable onchange: A callback function which will be called when user change the value of this input field.
  157. The ``onchange`` callback is invoked with one argument, the current value of input field.
  158. A typical usage scenario of ``onchange`` is to update other input item by using `input_update()`
  159. :param str placeholder: A hint to the user of what can be entered in the input. It will appear in the input field when it has no value set.
  160. :param bool required: Whether a value is required for the input to be submittable, default is ``False``
  161. :param bool readonly: Whether the value is readonly(not editable)
  162. :param list datalist: A list of predefined values to suggest to the user for this input. Can only be used when ``type=TEXT``
  163. :param str help_text: Help text for the input. The text will be displayed below the input field with small font
  164. :param other_html_attrs: Additional html attributes added to the input element.
  165. reference: https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/input#%E5%B1%9E%E6%80%A7
  166. :return: The value that user input.
  167. """
  168. item_spec, valid_func, onchange_func = _parse_args(locals(), excludes=('action',))
  169. # check input type
  170. allowed_type = {TEXT, NUMBER, FLOAT, PASSWORD, URL, DATE, TIME, COLOR, DATETIME_LOCAL}
  171. assert type in allowed_type, 'Input type not allowed.'
  172. value_setter = None
  173. if action:
  174. label, callback = action
  175. task_id = get_current_task_id()
  176. value_setter = Setter()
  177. def _set_value(value, label=value_setter):
  178. spec = {
  179. 'target_name': item_spec.get('name', 'data'),
  180. 'attributes': {'value': value}
  181. }
  182. if label is not value_setter:
  183. value_setter.label = label
  184. spec['attributes']['value'] = label
  185. spec['attributes']['readonly'] = True
  186. value_setter.value = value
  187. msg = dict(command='update_input', task_id=task_id, spec=spec)
  188. get_current_session().send_task_command(msg)
  189. callback_id = output_register_callback(lambda _: callback(_set_value))
  190. item_spec['action'] = dict(label=label, callback_id=callback_id)
  191. def preprocess_func(d): # Convert the original data submitted by the user
  192. if value_setter is not None and value_setter.label == d:
  193. return value_setter.value
  194. return d
  195. return single_input(item_spec, valid_func, preprocess_func, onchange_func)
  196. def textarea(label: str = '', *, rows: int = 6, code: Union[bool, Dict] = None, maxlength: int = None,
  197. minlength: int = None,
  198. validate: Callable[[Any], Optional[str]] = None, name: str = None, value: str = None,
  199. onchange: Callable[[Any], None] = None,
  200. placeholder: str = None, required: bool = None, readonly: bool = None, help_text: str = None,
  201. **other_html_attrs):
  202. r"""Text input area (multi-line text input)
  203. :param int rows: The number of visible text lines for the input area. Scroll bar will be used when content exceeds.
  204. :param int maxlength: The maximum number of characters (UTF-16 code units) that the user can enter.
  205. If this value isn't specified, the user can enter an unlimited number of characters.
  206. :param int minlength: The minimum number of characters (UTF-16 code units) required that the user should enter.
  207. :param dict/bool code: Enable a code style editor by providing the `Codemirror <https://codemirror.net/>`_ options:
  208. .. exportable-codeblock::
  209. :name: textarea-code
  210. :summary: `textarea()` code editor style
  211. res = textarea('Text area', code={
  212. 'mode': "python",
  213. 'theme': 'darcula'
  214. })
  215. put_code(res, language='python') # ..demo-only
  216. You can simply use ``code={}`` or ``code=True`` to enable code style editor.
  217. You can use ``Esc`` or ``F11`` to toggle fullscreen of code style textarea.
  218. Some commonly used Codemirror options are listed :ref:`here <codemirror_options>`.
  219. :param - label, validate, name, value, onchange, placeholder, required, readonly, help_text, other_html_attrs:
  220. Those arguments have the same meaning as for `input()`
  221. :return: The string value that user input.
  222. """
  223. item_spec, valid_func, onchange_func = _parse_args(locals())
  224. item_spec['type'] = TEXTAREA
  225. return single_input(item_spec, valid_func, lambda d: d, onchange_func)
  226. def _parse_select_options(options):
  227. # Convert the `options` parameter in the `select`, `checkbox`, and `radio` functions to a unified format
  228. # Available forms of option:
  229. # {value:, label:, [selected:,] [disabled:]}
  230. # (value, label, [selected,] [disabled])
  231. # value (label same as value)
  232. opts_res = []
  233. for opt in options:
  234. opt = copy.deepcopy(opt)
  235. if isinstance(opt, Mapping):
  236. assert 'value' in opt and 'label' in opt, 'options item must have value and label key'
  237. elif isinstance(opt, (list, tuple)):
  238. assert len(opt) > 1 and len(opt) <= 4, 'options item format error'
  239. opt = dict(zip(('label', 'value', 'selected', 'disabled'), opt))
  240. else:
  241. opt = dict(value=opt, label=opt)
  242. opts_res.append(opt)
  243. return opts_res
  244. def _set_options_selected(options, value):
  245. """set `selected` attribute for `options`"""
  246. if not isinstance(value, (list, tuple)):
  247. value = [value]
  248. for opt in options:
  249. if opt['value'] in value:
  250. opt['selected'] = True
  251. return options
  252. def select(label: str = '', options: List[Union[Dict[str, Any], Tuple, List, str]] = None, *, multiple: bool = None,
  253. validate: Callable[[Any], Optional[str]] = None, name: str = None, value: Union[List, str] = None,
  254. onchange: Callable[[Any], None] = None, native: bool = True, required: bool = None, help_text: str = None,
  255. **other_html_attrs):
  256. r"""Drop-down selection
  257. By default, only one option can be selected at a time, you can set ``multiple`` parameter to enable multiple selection.
  258. :param list options: list of options. The available formats of the list items are:
  259. * dict::
  260. {
  261. "label":(str) option label,
  262. "value":(object) option value,
  263. "selected":(bool, optional) whether the option is initially selected,
  264. "disabled":(bool, optional) whether the option is initially disabled
  265. }
  266. * tuple or list: ``(label, value, [selected,] [disabled])``
  267. * single value: label and value of option use the same value
  268. Attention:
  269. 1. The ``value`` of option can be any JSON serializable object
  270. 2. If the ``multiple`` is not ``True``, the list of options can only have one ``selected`` item at most.
  271. :param bool multiple: whether multiple options can be selected
  272. :param value: The value of the initial selected item. When ``multiple=True``, ``value`` must be a list.
  273. You can also set the initial selected option by setting the ``selected`` field in the ``options`` list item.
  274. :type value: list or str
  275. :param bool required: Whether to select at least one item, only available when ``multiple=True``
  276. :param bool native: Using browser's native select component rather than
  277. `bootstrap-select <https://github.com/snapappointments/bootstrap-select>`_. This is the default behavior.
  278. :param - label, validate, name, onchange, help_text, other_html_attrs: Those arguments have the same meaning as for `input()`
  279. :return: If ``multiple=True``, return a list of the values in the ``options`` selected by the user;
  280. otherwise, return the single value selected by the user.
  281. """
  282. assert options is not None, 'Required `options` parameter in select()'
  283. item_spec, valid_func, onchange_func = _parse_args(locals(), excludes=['value'])
  284. item_spec['options'] = _parse_select_options(options)
  285. if value is not None:
  286. item_spec['options'] = _set_options_selected(item_spec['options'], value)
  287. item_spec['type'] = SELECT
  288. return single_input(item_spec, valid_func=valid_func, preprocess_func=lambda d: d, onchange_func=onchange_func)
  289. def checkbox(label: str = '', options: List[Union[Dict[str, Any], Tuple, List, str]] = None, *, inline: bool = None,
  290. validate: Callable[[Any], Optional[str]] = None,
  291. name: str = None, value: List = None, onchange: Callable[[Any], None] = None, help_text: str = None,
  292. **other_html_attrs):
  293. r"""A group of check box that allowing single values to be selected/deselected.
  294. :param list options: List of options. The format is the same as the ``options`` parameter of the `select()` function
  295. :param bool inline: Whether to display the options on one line. Default is ``False``
  296. :param list value: The value list of the initial selected items.
  297. You can also set the initial selected option by setting the ``selected`` field in the ``options`` list item.
  298. :param - label, validate, name, onchange, help_text, other_html_attrs: Those arguments have the same meaning as for `input()`
  299. :return: A list of the values in the ``options`` selected by the user
  300. """
  301. assert options is not None, 'Required `options` parameter in checkbox()'
  302. item_spec, valid_func, onchange_func = _parse_args(locals(), excludes=['value'])
  303. item_spec['options'] = _parse_select_options(options)
  304. if value is not None:
  305. item_spec['options'] = _set_options_selected(item_spec['options'], value)
  306. item_spec['type'] = CHECKBOX
  307. return single_input(item_spec, valid_func, lambda d: d, onchange_func)
  308. def radio(label: str = '', options: List[Union[Dict[str, Any], Tuple, List, str]] = None, *, inline: bool = None,
  309. validate: Callable[[Any], Optional[str]] = None,
  310. name: str = None, value: str = None, onchange: Callable[[Any], None] = None, required: bool = None,
  311. help_text: str = None, **other_html_attrs):
  312. r"""A group of radio button. Only a single button can be selected.
  313. :param list options: List of options. The format is the same as the ``options`` parameter of the `select()` function
  314. :param bool inline: Whether to display the options on one line. Default is ``False``
  315. :param str value: The value of the initial selected items.
  316. You can also set the initial selected option by setting the ``selected`` field in the ``options`` list item.
  317. :param bool required: whether to must select one option. (the user can select nothing option by default)
  318. :param - label, validate, name, onchange, help_text, other_html_attrs: Those arguments have the same meaning as for `input()`
  319. :return: The value of the option selected by the user, if the user does not select any value, return ``None``
  320. """
  321. assert options is not None, 'Required `options` parameter in radio()'
  322. item_spec, valid_func, onchange_func = _parse_args(locals())
  323. item_spec['options'] = _parse_select_options(options)
  324. if value is not None:
  325. del item_spec['value']
  326. item_spec['options'] = _set_options_selected(item_spec['options'], value)
  327. # From https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/required
  328. # In the case of a same named group of radio buttons, if a single radio button in the group has the required attribute,
  329. # a radio button in that group must be checked, although it doesn't have to be the one with the attribute is applied
  330. if required is not None:
  331. del item_spec['required']
  332. item_spec['options'][-1]['required'] = required
  333. item_spec['type'] = RADIO
  334. return single_input(item_spec, valid_func, lambda d: d, onchange_func)
  335. def _parse_action_buttons(buttons):
  336. """
  337. :param label:
  338. :param actions: action list
  339. action available format:
  340. * dict: ``{label:button label, value:button value, [type: button type], [disabled:is disabled?]}``
  341. * tuple or list: ``(label, value, [type], [disabled])``
  342. * single value: label and value of button share the same value
  343. :return: dict format
  344. """
  345. act_res = []
  346. for act in buttons:
  347. act = copy.deepcopy(act)
  348. if isinstance(act, Mapping):
  349. assert 'label' in act, 'actions item must have label key'
  350. assert 'value' in act or act.get('type', 'submit') != 'submit' or act.get('disabled'), \
  351. 'actions item must have value key for submit type'
  352. elif isinstance(act, (list, tuple)):
  353. assert len(act) in (2, 3, 4), 'actions item format error'
  354. act = dict(zip(('label', 'value', 'type', 'disabled'), act))
  355. else:
  356. act = dict(value=act, label=act)
  357. act.setdefault('type', 'submit')
  358. assert act['type'] in ('submit', 'reset', 'cancel'), \
  359. "submit type must be 'submit'/'reset'/'cancel', not %r" % act['type']
  360. act_res.append(act)
  361. return act_res
  362. def actions(label: str = '', buttons: List[Union[Dict[str, Any], Tuple, List, str]] = None, name: str = None,
  363. help_text: str = None):
  364. r"""Actions selection
  365. It is displayed as a group of buttons on the page. After the user clicks the button of it,
  366. it will behave differently depending on the type of the button.
  367. :param list buttons: list of buttons. The available formats of the list items are:
  368. * dict::
  369. {
  370. "label":(str) button label,
  371. "value":(object) button value,
  372. "type":(str, optional) button type,
  373. "disabled":(bool, optional) whether the button is disabled,
  374. "color":(str, optional) button color
  375. }
  376. When ``type='reset'/'cancel'`` or ``disabled=True``, ``value`` can be omitted
  377. * tuple or list: ``(label, value, [type], [disabled])``
  378. * single value: label and value of button use the same value
  379. The ``value`` of button can be any JSON serializable object.
  380. ``type`` can be:
  381. * ``'submit'`` : After clicking the button, the entire form is submitted immediately,
  382. and the value of this input item in the final form is the ``value`` of the button that was clicked.
  383. ``'submit'`` is the default value of ``type``
  384. * ``'cancel'`` : Cancel form. After clicking the button, the entire form will be submitted immediately,
  385. and the form value will return ``None``
  386. * ``'reset'`` : Reset form. After clicking the button, the entire form will be reset,
  387. and the input items will become the initial state.
  388. Note: After clicking the ``type=reset`` button, the form will not be submitted,
  389. and the ``actions()`` call will not return
  390. The ``color`` of button can be one of: `primary`, `secondary`, `success`, `danger`, `warning`, `info`, `light`,
  391. `dark`.
  392. :param - label, name, help_text: Those arguments have the same meaning as for `input()`
  393. :return: If the user clicks the ``type=submit`` button to submit the form,
  394. return the value of the button clicked by the user.
  395. If the user clicks the ``type=cancel`` button or submits the form by other means, ``None`` is returned.
  396. When ``actions()`` is used as the last input item in `input_group()` and contains a button with ``type='submit'``,
  397. the default submit button of the `input_group()` form will be replace with the current ``actions()``
  398. **usage scenes of actions() **
  399. .. _custom_form_ctrl_btn:
  400. * Perform simple selection operations:
  401. .. exportable-codeblock::
  402. :name: actions-select
  403. :summary: Use `actions()` to perform simple selection
  404. confirm = actions('Confirm to delete file?', ['confirm', 'cancel'],
  405. help_text='Unrecoverable after file deletion')
  406. if confirm=='confirm': # ..doc-only
  407. ... # ..doc-only
  408. put_markdown('You clicked the `%s` button' % confirm) # ..demo-only
  409. Compared with other input items, when using `actions()`, the user only needs to click once to complete the submission.
  410. * Replace the default submit button:
  411. .. exportable-codeblock::
  412. :name: actions-submit
  413. :summary: Use `actions()` to replace the default submit button
  414. import json # ..demo-only
  415. # ..demo-only
  416. info = input_group('Add user', [
  417. input('username', type=TEXT, name='username', required=True),
  418. input('password', type=PASSWORD, name='password', required=True),
  419. actions('actions', [
  420. {'label': 'Save', 'value': 'save'},
  421. {'label': 'Save and add next', 'value': 'save_and_continue'},
  422. {'label': 'Reset', 'type': 'reset', 'color': 'warning'},
  423. {'label': 'Cancel', 'type': 'cancel', 'color': 'danger'},
  424. ], name='action', help_text='actions'),
  425. ])
  426. put_code('info = ' + json.dumps(info, indent=4))
  427. if info is not None:
  428. save_user(info['username'], info['password']) # ..doc-only
  429. if info['action'] == 'save_and_continue':
  430. add_next() # ..doc-only
  431. put_text('Save and add next...') # ..demo-only
  432. """
  433. assert buttons is not None, 'Required `buttons` parameter in actions()'
  434. item_spec, valid_func, onchange_func = _parse_args(locals())
  435. item_spec['type'] = 'actions'
  436. item_spec['buttons'] = _parse_action_buttons(buttons)
  437. return single_input(item_spec, valid_func, lambda d: d, onchange_func)
  438. def file_upload(label: str = '', accept: Union[List, str] = None, name: str = None, placeholder: str = 'Choose file',
  439. multiple: bool = False, max_size: Union[int, str] = 0, max_total_size: Union[int, str] = 0,
  440. required: bool = None, help_text: str = None, **other_html_attrs):
  441. r"""File uploading
  442. :param accept: Single value or list, indicating acceptable file types. The available formats of file types are:
  443. * A valid case-insensitive filename extension, starting with a period (".") character. For example: ``.jpg``, ``.pdf``, or ``.doc``.
  444. * A valid MIME type string, with no extensions.
  445. For examples: ``application/pdf``, ``audio/*``, ``video/*``, ``image/*``.
  446. For more information, please visit: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
  447. :type accept: str or list
  448. :param str placeholder: A hint to the user of what to be uploaded. It will appear in the input field when there is no file selected.
  449. :param bool multiple: Whether to allow upload multiple files. Default is ``False``.
  450. :param int/str max_size: The maximum size of a single file, exceeding the limit will prohibit uploading.
  451. The default is 0, which means there is no limit to the size.
  452. ``max_size`` can be a integer indicating the number of bytes, or a case-insensitive string ending with `K` / `M` / `G`
  453. (representing kilobytes, megabytes, and gigabytes, respectively).
  454. E.g: ``max_size=500``, ``max_size='40K'``, ``max_size='3M'``
  455. :param int/str max_total_size: The maximum size of all files. Only available when ``multiple=True``.
  456. The default is 0, which means there is no limit to the size. The format is the same as the ``max_size`` parameter
  457. :param bool required: Indicates whether the user must specify a file for the input. Default is ``False``.
  458. :param - label, name, help_text, other_html_attrs: Those arguments have the same meaning as for `input()`
  459. :return: When ``multiple=False``, a dict is returned::
  460. {
  461. 'filename': file name,
  462. 'content':content of the file (in bytes),
  463. 'mime_type': MIME type of the file,
  464. 'last_modified': Last modified time (timestamp) of the file
  465. }
  466. If there is no file uploaded, return ``None``.
  467. When ``multiple=True``, a list is returned. The format of the list item is the same as the return value when ``multiple=False`` above.
  468. If the user does not upload a file, an empty list is returned.
  469. .. note::
  470. If uploading large files, please pay attention to the file upload size limit setting of the web framework.
  471. When using :func:`start_server() <pywebio.platform.tornado.start_server>` or
  472. :func:`path_deploy() <pywebio.platform.path_deploy>` to start the PyWebIO application,
  473. the maximum file size to be uploaded allowed by the web framework can be set through the ``max_payload_size`` parameter.
  474. .. exportable-codeblock::
  475. :name: file_upload_example
  476. :summary: `file_upload()` example
  477. # Upload a file and save to server # ..doc-only
  478. f = input.file_upload("Upload a file") # ..doc-only
  479. open('asset/'+f['filename'], 'wb').write(f['content']) # ..doc-only
  480. imgs = file_upload("Select some pictures:", accept="image/*", multiple=True)
  481. for img in imgs:
  482. put_image(img['content'])
  483. """
  484. item_spec, valid_func, onchange_func = _parse_args(locals())
  485. item_spec['type'] = 'file'
  486. item_spec['max_size'] = parse_file_size(max_size) or platform_setting.MAX_PAYLOAD_SIZE
  487. item_spec['max_total_size'] = parse_file_size(max_total_size) or platform_setting.MAX_PAYLOAD_SIZE
  488. if platform_setting.MAX_PAYLOAD_SIZE:
  489. if item_spec['max_size'] > platform_setting.MAX_PAYLOAD_SIZE or \
  490. item_spec['max_total_size'] > platform_setting.MAX_PAYLOAD_SIZE:
  491. raise ValueError('The `max_size` and `max_total_size` value can not exceed the backend payload size limit. '
  492. 'Please increase the `max_total_size` of `start_server()`/`path_deploy()`')
  493. def read_file(data):
  494. for file in data:
  495. # Security fix: to avoid interpreting file name as path
  496. file['filename'] = os.path.basename(file['filename'])
  497. if not multiple:
  498. return data[0] if len(data) >= 1 else None
  499. return data
  500. return single_input(item_spec, valid_func, read_file, onchange_func)
  501. def slider(label: str = '', *, name: str = None, value: Union[int, float] = 0, min_value: Union[int, float] = 0,
  502. max_value: Union[int, float] = 100, step: int = 1, validate: Callable[[Any], Optional[str]] = None,
  503. onchange: Callable[[Any], None] = None, required: bool = None, help_text: str = None, **other_html_attrs):
  504. r"""Range input.
  505. :param int/float value: The initial value of the slider.
  506. :param int/float min_value: The minimum permitted value.
  507. :param int/float max_value: The maximum permitted value.
  508. :param int step: The stepping interval.
  509. Only available when ``value``, ``min_value`` and ``max_value`` are all integer.
  510. :param - label, name, validate, onchange, required, help_text, other_html_attrs: Those arguments have the same meaning as for `input()`
  511. :return int/float: If one of ``value``, ``min_value`` and ``max_value`` is float,
  512. the return value is a float, otherwise an int is returned.
  513. """
  514. item_spec, valid_func, onchange_func = _parse_args(locals())
  515. item_spec['type'] = 'slider'
  516. item_spec['float'] = any(isinstance(i, float) for i in (value, min_value, max_value))
  517. if item_spec['float']:
  518. item_spec['step'] = 'any'
  519. return single_input(item_spec, valid_func, lambda d: d, onchange_func)
  520. def input_group(label: str = '', inputs: List = None, validate: Callable[[Dict], Optional[Tuple[str, str]]] = None,
  521. cancelable: bool = False):
  522. r"""Input group. Request a set of inputs from the user at once.
  523. :param str label: Label of input group.
  524. :param list inputs: Input items.
  525. The item of the list is the call to the single input function, and the ``name`` parameter need to be passed in the single input function.
  526. :param callable validate: validation function for the group. If provided, the validation function will be called when the user submits the form.
  527. Function signature: ``callback(data) -> (name, error_msg)``.
  528. ``validate`` receives the value of the entire group as a parameter. When the form value is valid, it returns ``None``.
  529. When an input item's value is invalid, it returns the ``name`` value of the item and an error message.
  530. For example:
  531. .. exportable-codeblock::
  532. :name: input_group-valid_func
  533. :summary: `input_group()` form validation
  534. def check_form(data):
  535. if len(data['name']) > 6:
  536. return ('name', 'Name to long!')
  537. if data['age'] <= 0:
  538. return ('age', 'Age cannot be negative!')
  539. data = input_group("Basic info",[
  540. input('Input your name', name='name'),
  541. input('Repeat your age', name='age', type=NUMBER)
  542. ], validate=check_form)
  543. put_text(data['name'], data['age'])
  544. :param bool cancelable: Whether the form can be cancelled. Default is ``False``.
  545. If ``cancelable=True``, a "Cancel" button will be displayed at the bottom of the form.
  546. Note: If the last input item in the group is `actions()`, ``cancelable`` will be ignored.
  547. :return: If the user cancels the form, return ``None``, otherwise a ``dict`` is returned,
  548. whose key is the ``name`` of the input item, and whose value is the value of the input item.
  549. """
  550. assert inputs is not None, 'Required `inputs` parameter in input_group()'
  551. spec_inputs = []
  552. preprocess_funcs = {}
  553. item_valid_funcs = {}
  554. onchange_funcs = {}
  555. for single_input_return in inputs:
  556. input_kwargs = single_input_kwargs(single_input_return)
  557. assert all(
  558. k in (input_kwargs or {})
  559. for k in ('item_spec', 'preprocess_func', 'valid_func', 'onchange_func')
  560. ), "`inputs` value error in `input_group`. Did you forget to add `name` parameter in input function?"
  561. input_name = input_kwargs['item_spec']['name']
  562. assert input_name, "`name` can not be empty!"
  563. if input_name in preprocess_funcs:
  564. raise ValueError('Duplicated input item name "%s" in same input group!' % input_name)
  565. preprocess_funcs[input_name] = input_kwargs['preprocess_func']
  566. item_valid_funcs[input_name] = input_kwargs['valid_func']
  567. onchange_funcs[input_name] = input_kwargs['onchange_func']
  568. spec_inputs.append(input_kwargs['item_spec'])
  569. if all('auto_focus' not in i for i in spec_inputs): # No `auto_focus` parameter is set for each input item
  570. for i in spec_inputs:
  571. text_inputs = {TEXT, NUMBER, PASSWORD, SELECT, URL, FLOAT, DATE, TIME, DATETIME_LOCAL}
  572. if i.get('type') in text_inputs:
  573. i['auto_focus'] = True
  574. break
  575. spec = dict(label=label, inputs=spec_inputs, cancelable=cancelable)
  576. return input_control(spec, preprocess_funcs=preprocess_funcs,
  577. item_valid_funcs=item_valid_funcs,
  578. onchange_funcs=onchange_funcs,
  579. form_valid_funcs=validate)
  580. def parse_input_update_spec(spec):
  581. for key in spec:
  582. assert key not in {'action', 'buttons', 'code', 'inline', 'max_size', 'max_total_size', 'multiple', 'name',
  583. 'onchange', 'type', 'validate'}, '%r can not be updated' % key
  584. attributes = dict((k, v) for k, v in spec.items() if v is not None)
  585. if 'options' in spec:
  586. attributes['options'] = _parse_select_options(spec['options'])
  587. return attributes
  588. def input_update(name: str = None, **spec):
  589. """Update attributes of input field.
  590. This function can only be called in ``onchange`` callback of input functions.
  591. :param str name: The ``name`` of the target input item.
  592. Optional, default is the name of input field which triggers ``onchange``
  593. :param spec: The input parameters need to be updated.
  594. Note that those parameters can not be updated:
  595. ``type``, ``name``, ``validate``, ``action``, ``code``, ``onchange``, ``multiple``
  596. An example of implementing dependent input items in an input group:
  597. .. exportable-codeblock::
  598. :name: input-update
  599. :summary: Dependent input items in input group
  600. country2city = {
  601. 'China': ['Beijing', 'Shanghai', 'Hong Kong'],
  602. 'USA': ['New York', 'Los Angeles', 'San Francisco'],
  603. }
  604. countries = list(country2city.keys())
  605. location = input_group("Select a location", [
  606. select('Country', options=countries, name='country',
  607. onchange=lambda c: input_update('city', options=country2city[c])),
  608. select('City', options=country2city[countries[0]], name='city'),
  609. ])
  610. put_text(location) # ..demo-only
  611. """
  612. task_id = get_current_task_id()
  613. k = 'onchange_trigger-' + task_id
  614. if k not in get_current_session().internal_save:
  615. raise RuntimeError("`input_update()` can only be called in `onchange` callback.")
  616. trigger_name = get_current_session().internal_save[k]
  617. if name is None:
  618. name = trigger_name
  619. attributes = parse_input_update_spec(spec)
  620. send_msg('update_input', dict(target_name=name, attributes=attributes))