在 Python + Dash 中禁用“提交”按钮 30 秒

sem*_*lex 5 python plotly-dash

我想阻止用户按下“提交”按钮 30 秒,在他们将它推送到下面的脚本中之后。我该怎么做呢?这是我的代码目前的样子:

import dash
from dash.dependencies import Input, Output, State
import dash_core_components as dcc
import dash_html_components as html

app = dash.Dash()

app.layout = html.Div([
    dcc.Input(id='my-id', value='initial value', type="text"),
    html.Button('Submit', id='button'),
    html.Div(id='my-div')
])

@app.callback(
    Output(component_id='my-div', component_property='children'),
    [Input('button', 'n_clicks')],
    state=[State(component_id='my-id', component_property='value')]
)
def update_output_div(n_clicks, input_value):
    return 'You\'ve entered "{}" and clicked {} times'.format(input_value, n_clicks)

if __name__ == '__main__':
    app.run_server()
Run Code Online (Sandbox Code Playgroud)

有谁知道我如何阻止用户按下按钮 30 秒?

预先感谢。

编辑 15/08/2018 9:30AM GMT 对 stevepastelan 的回应:

import dash
from dash.dependencies import Input, Output, State
import dash_core_components as dcc
import dash_html_components as html

app = dash.Dash()

app.layout = html.Div([
    dcc.Input(id='my-id', value='initial value', type="text"),
    html.Button('Submit', id='button'),
    html.Div([dcc.Interval(
        id='interval-component',
        interval=1 * 3000,  # in milliseconds
        n_intervals=0
)]),
    html.Div(id='my-div')
])

@app.callback(
    Output(component_id='my-div', component_property='children'),
    [Input('button', 'n_clicks')], [Input('interval-component', 'n_intervals')],
    state=[State(component_id='my-id', component_property='value')]
)
def update_output_div(n_clicks,n_intervals, input_value):
    return 'You\'ve entered "{}" and clicked {} times'.format(input_value, n_clicks)

if __name__ == '__main__':
    app.run_server()
Run Code Online (Sandbox Code Playgroud)

编辑 15/08/2018 16:22PM 用编辑后的回调编写了更简单的脚本,但它不起作用:

import dash
from dash.dependencies import Input, Output, State
import dash_core_components as dcc
import dash_html_components as html

app = dash.Dash()

app.layout = html.Div([
    dcc.Input(id='my-id', value='initial value', type="text"),
    html.Button('Submit', id='button'),
    html.Div([dcc.Interval(
        id='interval-component',
        interval=1 * 3000,  # in milliseconds
        n_intervals=0
)]),
    html.Div(id='my-div')
])

@app.callback(
    Output(component_id='my-div', component_property='children'),
    [Input('button', 'n_clicks')], [Input('interval-component', 'n_intervals')],
    state=[State(component_id='my-id', component_property='value')]
)

def update_output_div(n_clicks,n_intervals, input_value):
    return 'You\'ve entered "{}" and clicked {} times'.format(input_value, n_clicks)

if __name__ == '__main__':
    app.run_server()
Run Code Online (Sandbox Code Playgroud)

ste*_*lan 3

更新的答案

好吧,我设法实现了我自己的建议,但这并不是微不足道的,而且仍然有一些怪癖。

复杂的因素是:

  • Dash 不允许两个回调指向同一目标Output
  • 没有好的方法来跟踪哪个InputEvent触发了您的回调。解决方法通常涉及跟踪每个按钮的点击次数(请参阅https://github.com/plotly/dash-html-components/pull/37例如,
  • disable=True通过或禁用计时器max_requests=0似乎是永久性的。一旦我以这种方式停止了计时器,我就无法使用 或 重新启动disable=Falsemax_requests=1000

问题:

  • 在这个解决方案中,update_output_div()被调用两次 - 但您可以通过测量按钮点击次数与之前的计数来区分两者之间的差异,这样您就可以防止它两次提交数据。
  • 小于 100 毫秒的超时将不起作用。我必须拆分延迟计时器才能使我的方法正常工作,因此我选择100(1000 * BUTTON_PRESS_LOCKOUT_SECONDS)-100作为两个计时器持续时间。原则上,你可以将它们平均分成两半。我不知道通过网络工作时使用低超时是否有任何问题(我在本地主机上进行了测试)。

灵感源自:

import json
import datetime

import dash
from dash.dependencies import Input, Output, State, Event
import dash_core_components as dcc
import dash_html_components as html

BUTTON_PRESS_LOCKOUT_SECONDS = 10  # seconds

app = dash.Dash()
app.config['suppress_callback_exceptions']=True

def serve_layout():
    return html.Div([
        dcc.Input(id='my-id', value='initial value', type="text"),
        html.Button('Submit', id='button'),
        html.Div(
              [
                  dcc.Interval(id='interval-component', disabled=True)
                , dcc.Interval(id='interval-sync-component', disabled=True)
              ]
            , id='interval-container'
        ),
        html.Div("", id='my-div'),
        html.Div(json.dumps({'n_clicks':0, 'n_previous_clicks':0}), id='local_data'),
        html.Div('??', id='button-status'),
    ])

app.layout = serve_layout

# Track button clicks
@app.callback(
    output=Output(component_id='local_data', component_property='children'),
    inputs=[Input('button', 'n_clicks')],
    state=[State('local_data', 'children')],
    events=[Event('interval-sync-component', 'interval')]
)
def track_clicks(n_clicks, local_data_json):
    if n_clicks is None:
        n_clicks = 0

    local_data = json.loads(local_data_json)
    n_previous_clicks = local_data['n_clicks']

    # Update local data with the new click data
    local_data.update(**{'n_clicks': n_clicks, 'n_previous_clicks': n_previous_clicks})
    # local_data.update(**{'n_clicks': n_clicks, 'n_previous_clicks': n_previous_clicks})
    return json.dumps(local_data)


# When the button click count is updated, submit
@app.callback(
    output=Output(component_id='my-div', component_property='children'),
    inputs=[Input('local_data', 'children')],
    state=[State(component_id='my-id', component_property='value'), State('my-div', 'children')]
)
def update_output_div(local_data_json, input_value, current_state):
    local_data = json.loads(local_data_json)
    n_clicks = local_data['n_clicks']
    n_previous_clicks = local_data['n_previous_clicks']

    # Real submit
    if n_clicks > n_previous_clicks:
        return 'You\'ve entered "{}" and clicked {} times ({})'.format(
              input_value
            , n_clicks if n_clicks is not None else 0
            , datetime.datetime.now()
        )

    # Not a real submit, but function is called an extra time as a side effect of the timer nonsense below.
    else:
        return '*' + current_state


# Start (or stop) the timer
@app.callback(
    output=Output('interval-container', 'children'),
    inputs=[Input('local_data', 'children')],
    state=[State('button', 'disabled')],
    events=[Event('interval-component', 'interval')]
)
def start_timer(local_data_json, button_is_disabled):
    local_data = json.loads(local_data_json)
    n_clicks = local_data['n_clicks']
    n_previous_clicks = local_data['n_previous_clicks']

    children=[]

    if n_clicks > n_previous_clicks:
        sync_timer = dcc.Interval(
            id='interval-sync-component',
            interval=100,  # in milliseconds
        )
        children.append(sync_timer)

    if button_is_disabled:
        main_timer = dcc.Interval(
            id='interval-component',
            interval=(1000 * BUTTON_PRESS_LOCKOUT_SECONDS)-100,  # in milliseconds
        )
        children.append(main_timer)

    return children


# Enable the button whenever the timer interval is triggered or disable it when the button is pressed
@app.callback(
    output=Output('button', 'disabled'),
    inputs=[Input('button', 'n_clicks')],
    state=[State('local_data', 'children')],
    events=[Event('interval-component', 'interval')]
)
def toggle_button_disabled_state(n_clicks, local_data_json):
    local_data = json.loads(local_data_json)
    # n_clicks = local_data['n_clicks']
    if n_clicks is None:
        n_clicks = 0
    n_previous_clicks = local_data['n_previous_clicks']

    # We got here via button click, so disable the button
    if n_clicks > n_previous_clicks:
        return True

    # We got here via timer expiration, so enable the button
    else:
        return False  # enable the button

# Report on the button status
@app.callback(
    output=Output('button-status', 'children'),
    inputs=[Input('button', 'disabled')]
)
def update_button_status(disabled):
    if disabled:
        return 'Disabled submit button for {} seconds'.format(BUTTON_PRESS_LOCKOUT_SECONDS)
    else:
        return 'Submit button enabled'


if __name__ == '__main__':
    app.run_server()
Run Code Online (Sandbox Code Playgroud)

原答案

您可以使用 来根据计时器触发要在页面上执行的操作dash_core_components.Interval。这里有一些例子: https: //dash.plot.ly/live-updates

您可以使用 初始化间隔组件n_intervals = 0,然后使提交按钮禁用并设置n_intervals = 1。然后在重新启用按钮的时间间隔上编写回调。