Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix issue reusing FileDownload model #4328

Merged
merged 2 commits into from
Jan 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 10 additions & 21 deletions panel/models/file_download.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,11 @@ export class FileDownloadView extends InputWidgetView {
anchor_el: HTMLAnchorElement
_downloadable: boolean = false
_click_listener: any
_embed: boolean = false
_prev_href: string | null = ""
_prev_download: string | null = ""

protected input_el: HTMLInputElement

initialize(): void {
super.initialize()
if ( this.model.data && this.model.filename ) {
this._embed = true
}
}

connect_signals(): void {
super.connect_signals()
this.connect(this.model.properties.button_type.change, () => this._update_button_style())
Expand Down Expand Up @@ -72,28 +64,25 @@ export class FileDownloadView extends InputWidgetView {

// Changing the disabled property calls render() so it needs to be handled here.
// This callback is inherited from ControlView in bokehjs.
if ( this.model.disabled ) {
if (this.model.disabled) {
this.anchor_el.setAttribute("disabled", "")
this._downloadable = false
} else {
this.anchor_el.removeAttribute("disabled")
// auto=False + toggle Disabled ==> Needs to reset the link as it was.
if ( this._prev_download ) {
if (this._prev_download)
this.anchor_el.download = this._prev_download
}
if ( this._prev_href ) {
if (this._prev_href)
this.anchor_el.href = this._prev_href
}
if ( this.anchor_el.download && this.anchor_el.download ) {
if (this.anchor_el.download && this.anchor_el.download)
this._downloadable = true
}
}

// If embedded the button is just a download link.
// Otherwise clicks will be handled by the code itself, allowing for more interactivity.
if ( this._embed ) {
if (this.model.embed)
this._make_link_downloadable()
} else {
else {
// Add a "click" listener, note that it's not going to
// handle right clicks (they won't increment 'clicks')
this._click_listener = this._increment_clicks.bind(this)
Expand All @@ -120,16 +109,14 @@ export class FileDownloadView extends InputWidgetView {
}

_handle_click() : void {

// When auto=False the button becomes a link which no longer
// requires being updated.
if ( !this.model.auto && this._downloadable) {
if ( !this.model.auto && this._downloadable)
return
}

this._make_link_downloadable()

if ( !this._embed && this.model.auto ) {
if ( !this.model.embed && this.model.auto ) {
// Temporarily removing the event listener to emulate a click
// event on the anchor link which will trigger a download.
this.anchor_el.removeEventListener("click", this._click_listener)
Expand Down Expand Up @@ -201,6 +188,7 @@ export namespace FileDownload {
button_type: p.Property<ButtonType>
clicks: p.Property<number>
data: p.Property<string | null>
embed: p.Property<boolean>
label: p.Property<string>
filename: p.Property<string | null>
_transfers: p.Property<number>
Expand All @@ -225,6 +213,7 @@ export class FileDownload extends InputWidget {
auto: [ Boolean, false ],
clicks: [ Int, 0 ],
data: [ Nullable(String), null ],
embed: [ Boolean, false ],
label: [ String, "Download" ],
filename: [ Nullable(String), null ],
button_type: [ ButtonType, "default" ], // TODO (bev)
Expand Down
2 changes: 2 additions & 0 deletions panel/models/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ class FileDownload(InputWidget):

data = String(help="""Encoded URI data.""")

embed = Bool(False, help="""Whether the data is pre-embedded.""")

label = String("", help="""The text label for the button to display.""")

filename = String(help="""Filename to use on download""")
Expand Down
63 changes: 63 additions & 0 deletions panel/tests/ui/widgets/test_misc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import io
import sys
import tempfile
import time

import param
import pytest

from panel.io.server import serve
from panel.layout import Column, Tabs
from panel.widgets import FileDownload, TextInput

pytestmark = pytest.mark.ui
not_windows = pytest.mark.skipif(sys.platform=='win32', reason="Does not work on Windows")


@not_windows
def test_file_download_updates_when_navigating_between_dynamic_tabs(page, port):
text_input = TextInput(value='abc')

@param.depends(text_input.param.value)
def create_file(value):
return io.StringIO(value)

download = FileDownload(
callback=create_file, filename='f.txt', embed=False
)

tabs = Tabs(
("Download", Column(text_input, download)),
("Dummy", "dummy tab"),
dynamic=True
)

serve(tabs, port=port, threaded=True, show=False)

time.sleep(0.5)

page.goto(f"http://localhost:{port}")

with page.expect_download() as download_info:
page.click('a.bk-btn')

download = download_info.value
tmp = tempfile.NamedTemporaryFile(suffix='.txt')
download.save_as(tmp.name)
assert tmp.file.read().decode('utf-8') == 'abc'

page.click('.bk-tab:not(.bk-active)')
page.click('.bk-tab:not(.bk-active)')

page.click('.bk-input')

page.keyboard.type('def')
page.keyboard.press('Enter')

with page.expect_download() as download_info:
page.click('a.bk-btn')

download = download_info.value
tmp = tempfile.NamedTemporaryFile(suffix='.txt')
download.save_as(tmp.name)
assert tmp.file.read().decode('utf-8') == 'abcdef'
3 changes: 1 addition & 2 deletions panel/widgets/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,7 @@ class FileDownload(Widget):
}

_rename: ClassVar[Mapping[str, str | None]] = {
'callback': None, 'embed': None, 'file': None,
'_clicks': 'clicks', 'name': 'title'
'callback': None, 'file': None, '_clicks': 'clicks', 'name': 'title'
}

_widget_type: ClassVar[Type[Model]] = _BkFileDownload
Expand Down