Skip to content

Show file download/upload/print affordances in app context - #15050

Merged
rtibbles merged 6 commits into
learningequality:developfrom
rtibblesbot:issue-14654-9ea726
Jul 29, 2026
Merged

Show file download/upload/print affordances in app context#15050
rtibbles merged 6 commits into
learningequality:developfrom
rtibblesbot:issue-14654-9ea726

Conversation

@rtibblesbot

@rtibblesbot rtibblesbot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

  • isAppContext gates hid file upload/download/print affordances in installed apps — a workaround from when apps couldn't handle downloads. Removes the four gates so the affordances render in every context.
  • Affordances: content "Save to device", coach CSV export, facility CSV import/export + log downloads, "Print all passwords".

Once visible in-app, several of them didn't actually work. Fixed here:

  • Facility CSV downloads did nothing. window.open(url, '_blank') is a no-op in the webview. New DataPage/downloadCsvFile.js clicks a synthetic <a download> instead — the same idiom the coach export already used via csv-generator-client.
  • The CSV import picker greyed out every file on Android. onShowFileChooser passed the web accept entries straight into OpenDocument, which filters on EXTRA_MIME_TYPES — a bare .csv is not a MIME type and matches nothing. New FileChooserUtils.toPickerMimeTypes resolves extensions through MimeTypeMap, the same map document providers type files with. The input lists accept="text/csv,.csv" so providers that report a type rather than derive one also match.
  • The Mac/Linux app saved nothing. The WebKit backends wxPython's html2 uses ignore an anchor's download: an http(s) link navigated the webview to the CSV in place of the page, and the data: URLs the coach exporter builds were dropped entirely. An injected script now intercepts a[download] clicks, reads the resource in the page where the session cookie lives, and hands the bytes to Python to write out through a save dialog. WebView2 has its own download UI, so Windows keeps only the print shim it had. Bridge messages are JSON keyed on type now that there is more than one.
  • Android downloads looked stuck. The download leaves the page as soon as the button is pressed and finishes in the notification shade, so the page shows nothing. Toast on start, completion and failure.

References

Fixes #14654. Android WebView download support: learningequality/kolibri-installer-android#267.

Reviewer guidance

Confirm each of:

  • Printing
  • File upload (e.g. user import)
  • File download (e.g. log file download)

On each of:

  • Android app
  • Mac App
  • Windows App

AI usage

Used Claude Code to remove the gates and update tests following a pre-approved plan. Verified with the affected Jest, pytest and JVM suites and prek lint. The four platform fixes came out of manual QA in the Windows, Mac and Android apps.

Deviations from the issue spec

  • Adds the four platform fixes above, which the issue's change list doesn't mention. Removing the gates alone would have shipped a visible-but-broken CSV download, import picker and Mac/Linux save path in the apps.

@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

How was this generated?
  • Ran pre-flight CI checks (lint, format, tests) and verified all pass
  • Rebased onto the target branch and resolved any conflicts
  • Reorganized commit history into clean, logical commits
  • Audited the diff to ensure only issue-relevant files are changed
  • Built PR body from the repository's PR template with evidence blocks
@rtibblesbot

🟡 Waiting for feedback

Last updated: 2026-07-29 17:12 UTC

@github-actions github-actions Bot added APP: Facility Re: Facility App (user/class management, facility settings, csv import/export, etc.) APP: Coach Re: Coach App (lessons, quizzes, groups, reports, etc.) DEV: frontend SIZE: small labels Jul 23, 2026
@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

npm Package Versions

Warning

The following packages have changed files but no version bump:

Package Version Changed files
kolibri 0.18.0 2

If these changes affect published code, consider bumping the version.

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

@rtibblesbot
rtibblesbot marked this pull request as ready for review July 23, 2026 03:29

@rtibbles rtibbles left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A little bit more cleanup, but the code here looks broadly sensible.

Comment thread kolibri/plugins/coach/frontend/views/common/ReportsControls.vue Outdated
Comment thread packages/kolibri/components/DownloadButton.vue Outdated

@rtibbles rtibbles left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just one more bit of cleanup, then please tidy up the commit history.

Comment thread packages/kolibri/components/__tests__/DownloadButton.spec.js Outdated

@rtibbles rtibbles left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code makes sense - needs commit history tidy up, ready for manual QA though.

@rtibblesbot

Copy link
Copy Markdown
Contributor Author

Tidied the commit history — squashed the four per-component commits into a single commit (Show file upload/download/print affordances in app context) since this is one cohesive change removing the isAppContext gates. The tree is byte-identical to the previous head; only the history changed. Ready for manual QA.

@radinamatic

Copy link
Copy Markdown
Member

EXE installed on Windows 11:

  • Printing lesson and coach reports, as well as the passwords working correctly ✔️

    Desktop Icons 2_025
  • File upload (CSV with users) import also working as expected ✔️

  • File download: CSV file download for quizzes and lessons reports is successful ✔️, however, summary and session logs, as well as the generated new user CSV file, cannot be downloaded from the app UI, although all features work when Kolibri is opened in the browser.

kolibri-app.zip

@radinamatic

Copy link
Copy Markdown
Member

APK installed on Redmi10C with Android 13:

All printing/saving as PDF, plus saving/downloading CSV files is working beautifully, even those on the Facility/Data page that are failing in Windows app! 🎉 🎆

And I even had the Android Studio ready and connected to the device to capture any potential issues... 😅

Just need to restart and try the CSV file import feature, but looking great for now 👏🏽

@rtibbles rtibbles self-assigned this Jul 27, 2026
@radinamatic

Copy link
Copy Markdown
Member

Not able to import a CSV file to Kolibri installed on Android: the button Choose file responds correctly, opens the device file manager, but the corresponding CSV file seems grayed out and nothing happens when it's tapped 🤔

It happens for both the CSV files copied directly to the device's internal storage (so technically no downloading would be required), and the CSV file uploaded to a Google Drive (I can see the CSV file to select, but nothing happens when I tried to tap it to import).

Tried setting up both as OnMyOwn and GroupLearning, just in case something would be different in the logcats.

logs.zip

Screenshot_2026-07-27-18-23-01-995_com google android documentsui

@rtibblesbot

Copy link
Copy Markdown
Contributor Author

Thanks — both traced to a concrete cause and are fixed in 1ba7521 and 6c19c40. Needs a fresh build to re-verify on device.

Windows downloads. Session logs, summary logs and the generated user CSV were the only three call sites using window.open(url, '_blank'); the app webview opens no new window, so nothing happened. The exports that worked for you click an <a download> instead — all three now use that same mechanism.

Android CSV import. accept=".csv" is a browser-only shorthand. The webview passes the first accept entry to the system file picker as a MIME type, and nothing matches the literal string .csv, so every file greys out. Now accept="text/csv,.csv".

If Android still greys out CSVs after this build, the chooser intent is built shell-side and belongs to kolibri-installer-android#267.

Several file upload/download/print UI affordances were gated behind
isAppContext, hiding them in the installed apps. That was a workaround
for the assumption that apps could not handle file downloads; the
Windows and Mac apps handle this natively and the Android WebView work
is tracked in kolibri-installer-android#267. Remove the gates so the
affordances render regardless of context:

- DownloadButton "Save to device" button visibility no longer checks
  isAppContext.
- ReportsControls coach report CSV export drops isAppContext from
  exportDisabled (and the stale "disable in app mode" comment).
- DataPage facility CSV import/export and session/summary log download
  sections drop the canUploadDownloadFiles gate.
- AllPasswordsPage "Print all passwords" button always renders and the
  title row always uses the two-column layout, on both the coach and
  facility passwords pages.

Behaviour in non-app context is unchanged. Blocking Jest tests are
removed and replaced with app-context render assertions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@radinamatic

Copy link
Copy Markdown
Member

Fix on Windows is confirmed, downloading CSV files is now working for all the features on Facility - Data page.

Win11 2606 (start)  Running  - Oracle VirtualBox_035

@radinamatic

Copy link
Copy Markdown
Member

Still hitting some bumps with importing CSV in Kolibri on Android:

  • CSV files located on the device are still grayed out, could it be a permission issue?
  • CSV on the GDrive was not grayed out and it was possible to select. However, once I pressed the Continue button, server seems to disconnect and it was not possible to proceed.

logs.zip
logcat from Android Studio are included.

Screenshot_2026-07-28-00-36-21-044_org learningequality Kolibri

@rtibblesbot

Copy link
Copy Markdown
Contributor Author

Thanks — the logcat had both answers in it. One is fixed here in de97ef7; the other is a separate bug I've filed as #15094.

Greyed-out local CSVs — fixed. I had this wrong when I said it was the accept attribute. WebViewActivity.onShowFileChooser passed the accept entries straight into OpenDocument, which filters on EXTRA_MIME_TYPES:

String[] mimeTypes = Arrays.stream(accepted != null ? accepted : new String[0])
    .filter(t -> t != null && !t.isEmpty())
    .toArray(String[]::new);

.csv is not a MIME type, so it matched nothing; text/csv was doing all the filtering. That is exactly the split you saw — Drive reports text/csv and stayed selectable, while local files get their type from MimeTypeMap, which maps .csv to something else. The picker now resolves extensions through that same MimeTypeMap, so a local file and the filter always agree. Covered by FileChooserUtilsTest.

Server disconnect on Continue — not this PR. Opening the picker backgrounds the activity, and after ~60s Android stops the server:

00:34:43.896 ActivityManager W Stopping service due to app idle: u0a1 -3m16s676ms
                                org.learningequality.Kolibri/.KolibriServerService

It then comes back on a different port (46655 → 45791) and the shell reloads the WebView, which discards the picked file. So the file selection was fine; the page underneath it was replaced. That's a service-lifecycle problem affecting any handoff to another activity, not just CSV import, so I've filed #15094 rather than widen this PR.

That means CSV import on Android still won't complete until #15094 lands — worth knowing before you re-test. What this build should show is that local CSVs are selectable rather than greyed. If you pick one quickly enough to stay inside the idle window, the import should go through.

Needs a fresh build.

@rtibblesbot

Copy link
Copy Markdown
Contributor Author

One follow-up on that: the new FileChooserUtilsTest would not have run in CI. tox.yml's path filter is ["**.py", ".github/workflows/tox.yml", "pyproject.toml", "uv.lock", "python_packages/**"], so a Java-only change under platforms/ skips every job gated on it — including the Android JVM tests added in #15075, which exist to cover exactly that code. Added platforms/** in ee55fb7.

Shout if you'd rather that went in its own PR.

@github-actions github-actions Bot added the DEV: dev-ops Continuous integration & deployment label Jul 27, 2026
@pcenov

pcenov commented Jul 28, 2026

Copy link
Copy Markdown
Member

Mac app:

  1. Going to Facility > Data and clicking any of the available file download buttons results in directly opening the .csv file inside the Mac app instead of actually downloading the file to the device.
  2. The Export as CSV button is not working it all of the Coach pages - nothing happens when I click it.

Here's a video for both issues:

mac.app.mp4

Mac app logs: mac-app-logs.zip

The Print report buttons and the user import option are working correctly.

Android app:

  1. Clicking any of the download buttons at Facility > Data results in seeing the button in a pressed state without a clear indication that the download has started and completed. An experienced user can see that in the notification area but the user experience is not great. The same is valid for the Coach reports where clicking the Export as CSV button leaves it in a pressed state with the 'Export as CSV' tooltip remaining visible.
Android.download.mp4

@github-actions github-actions Bot added DEV: backend Python, databases, networking, filesystem... SIZE: large and removed DEV: backend Python, databases, networking, filesystem... SIZE: medium labels Jul 28, 2026
@rtibblesbot

Copy link
Copy Markdown
Contributor Author

Thanks — the two Mac problems are one cause, and both are fixed in 911cd52. The Android feedback point is 2c68bda.

Mac. wxPython's html2 uses WebKit on macOS, and that backend has no download support at all: an anchor's download attribute is simply ignored. So the Facility CSVs navigate the webview to the file and it renders as text in place of the page, and the coach export — whose link carries a data: URL with target="_blank" — hits WebKit's block on top-level data: navigation and does nothing whatsoever. Same missing capability, two different-looking symptoms.

The app now intercepts a[download] clicks in an injected script, reads the resource in the page (where the session cookie lives, so authenticated exports work), and hands the bytes to Python over the existing kolibriBridge to write out. What you'll see that's new: a Save dialog, defaulting to ~/Downloads and to the filename the server or the exporter supplied. Windows is untouched — WebView2 has its own download UI and you and @radinamatic have both already confirmed it works.

I have no macOS here, so this one is unverified beyond unit tests on the message handling. It needs a fresh DMG.

Android download feedback. Fair — the notification shade is the only signal today. There is now a toast on start, on completion and on failure, so something lands in front of the user without them going looking.

Android stuck button + tooltip. This one I'm leaving alone, and I don't think it belongs to this PR. It's sticky :hover — a touch device synthesises a hover on tap and holds it until you tap elsewhere, so the button keeps its hover styling and KTooltip (which is trigger="hover") keeps showing. You'll get the same behaviour tapping any icon button in Chrome on Android. The fix would be in kolibri-design-system, not here; happy to file it there if you'd like.

Both platforms need fresh builds.


Separately, a correction to my earlier note about tox.yml: I was wrong that adding platforms/** would make FileChooserUtilsTest run in CI. Nothing in .github/workflows invokes ./gradlew test — the platform_tests matrix is pytest for platforms/debian-server and platforms/desktop-app only, so the Android JVM tests run nowhere. All that entry actually did was un-skip the whole Python matrix, macOS rows included, on Java-only changes. Reverted in 1cf19f3.

@github-actions github-actions Bot added the DEV: backend Python, databases, networking, filesystem... label Jul 28, 2026
@radinamatic

radinamatic commented Jul 29, 2026

Copy link
Copy Markdown
Member

Confirming that importing CSV files in latest Kolibri APK asset on Android is now working correctly for files located on the device. ✔️

However, the previously reported issue with server disconnect when trying to import the CSV file on Google Drive is still extant. Disregard, acknowledging that a follow up issue was filed.

@pcenov

pcenov commented Jul 29, 2026

Copy link
Copy Markdown
Member

Mac app: I confirm both fixes - clicking a Download or Export as CSV button results in a Save dialog, defaulting to ~/Downloads:

mac-save.mp4

Android app: There's now a toast message which should be good enough for now:

Screenshot_20260729_110656

Windows app: No changes.

@radinamatic radinamatic left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTQATeam!!! 💯 :shipit: 🚀

@rtibbles rtibbles left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The manual QA shows this is working, so mostly I am just interested in some cleanup here, while preserving the code as is.

If any of the tests are not giving us helpful coverage they should be removed or replaced.

When things have been fixed up, we should tidy up the commit history to give a logical story of how things are.

# backend; intercept it and route to ICoreWebView2_16::ShowPrintUI via
# webview2_native.
PRINT_SCRIPT = _bridge_script(
"""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels concise enough to be inline, so this is OK.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left inline as-is.

# exporter builds, is dropped entirely. Read the resource in the page, where the
# session cookie lives, and hand the bytes to Python to write out.
DOWNLOAD_SCRIPT = _bridge_script(
r"""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this script is extensive enough that I would prefer it be stored in a .js file and loaded to be used. If needs be. It may need some linting exclusion exceptions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to src/kolibri_app/scripts/download.js, read at import time via importlib.resources.files and passed through the same __BRIDGE__/__TYPE__ substitution as the inline print shim. Added the directory to datas_list in kolibri.spec so PyInstaller bundles it.

No lint exclusion turned out to be needed: lint-frontend globs {kolibri*/**/frontend,packages,build_tools}/**/*.{js,vue,...}, so nothing under platforms/ is linted (verified with a deliberately broken probe file — prek passed it). I kept a /* eslint-disable no-console */ at the top of the file anyway so editor ESLint integrations, which resolve the root config regardless of that glob, do not flag the console.error in the catch.

@@ -0,0 +1,98 @@
import base64

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have been iffy in the past about tests for the desktop app, as they have involved significant mocking of the wx APIs - how much value are these actually adding?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair question. I went through them and cut the ones that were really only asserting the shape of a mock call:

  • test_offers_the_sanitised_name_as_the_default — deleted. It asserted wx.FileDialog.call_args.kwargs["defaultFile"], which is a mock-shape assertion that duplicates the direct _download_filename tests.
  • test_print_message_defers_the_dialog — folded into test_repeated_print_messages_enqueue_one_dialog, which now asserts the target as well as the count, so nothing is lost.
  • Added TestBridgeScripts — no wx at all; it catches scripts/download.js failing to load or shipping with the __BRIDGE__/__TYPE__ placeholders unsubstituted, which is now a real packaging failure mode after the change above.

On the mocking: the module-level sys.modules wx stub is not new here — it is the pattern already in test_kolibri_home_readable.py on develop; this branch only hoisted it into conftest.py so both modules share it. Beyond that the tests touch exactly two wx names, wx.CallAfter (a scheduling primitive) and wx.FileDialog (the OS file picker — a hard boundary), and what they assert is our logic, not wx:

  • TestDownloadFilename — the path-traversal guard on a page-supplied filename. Pure function, no wx, and the one place a bug is a security bug.
  • TestScriptMessages — the bridge dispatch contract: malformed or unknown JSON from the page must not blow up the handler, and repeated window.print() calls must not stack up dialogs (that coalescing is the non-obvious bit).
  • TestSaveDownload — writes real bytes to a real temp file and asserts the content; the mock only stands in for the picker returning a path. Also covers "cancel writes nothing" and "undecodable base64 never opens the dialog".

If you would still rather not carry the two wx-touching classes, I am happy to drop TestScriptMessages and TestSaveDownload and keep only TestDownloadFilename and TestBridgeScripts — just say the word.

@rtibblesbot
rtibblesbot force-pushed the issue-14654-9ea726 branch 3 times, most recently from 7872f77 to 843cc0f Compare July 29, 2026 16:26

@rtibbles rtibbles left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QA checks out, post-QA code changes minimal and make sense.

rtibblesbot and others added 5 commits July 29, 2026 10:10
The session log, summary log and user CSV downloads called
window.open(url, '_blank'). The installed apps render Kolibri in an
embedded webview that does not open new windows, so nothing happened —
QA saw exactly these three downloads fail on Windows while every other
CSV export worked, because the others click an <a download> instead.

Route all three through a shared downloadCsvFile helper that uses that
same link-click mechanism.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
accept=".csv" is a browser-only shorthand. The installed apps' webviews
pass the first accept entry straight to the system file picker as a MIME
type, and no file matches the literal string ".csv" — on Android QA saw
the picker open with every CSV greyed out. Put text/csv first; browsers
take either form.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
onShowFileChooser passed the web accept entries straight into
OpenDocument, which filters on EXTRA_MIME_TYPES. A bare ".csv" is not a
MIME type and matches no document, so it greyed out every file the
picker offered.

Android's document providers derive a file's type from its extension via
MimeTypeMap, so resolving accept extensions through that same map is what
makes those files match. This is why QA saw the split: Drive reports
text/csv and stayed selectable, while local files reported whatever
MimeTypeMap maps .csv to and greyed out.

The comment added alongside accept="text/csv,.csv" said webviews use only
the first accept entry. The logcat shows all entries reaching the intent,
so it is replaced with the actual reason both forms are listed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The WebKit backends wxPython's html2 uses on macOS and Linux have no
download support: an anchor's `download` attribute is ignored, so the
click either navigates the webview to the file — showing a facility CSV
as text in place of the page — or, for the data: URLs the coach report
exporter builds, is dropped on the floor and nothing happens at all.

Intercept `a[download]` clicks in an injected script, read the resource
in the page where the session cookie lives, and hand the bytes to Python
to write out through a save dialog. WebView2 already has its own
download UI, so Windows keeps the print shim it had and nothing else.

Bridge messages are JSON objects keyed on "type" now that there is more
than one of them.
A download leaves the page as soon as the button is pressed and finishes
in the notification shade, so all the page itself shows is a button that
looks stuck. Toast on start, completion and failure so the feedback lands
where the user is already looking.
@rtibbles
rtibbles merged commit 0d6830f into learningequality:develop Jul 29, 2026
89 of 90 checks passed
@rtibblesbot
rtibblesbot deleted the issue-14654-9ea726 branch July 29, 2026 18:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

APP: Coach Re: Coach App (lessons, quizzes, groups, reports, etc.) APP: Facility Re: Facility App (user/class management, facility settings, csv import/export, etc.) DEV: backend Python, databases, networking, filesystem... DEV: dev-ops Continuous integration & deployment DEV: frontend SIZE: large

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Remove isAppContext gates on file upload/download capabilities

4 participants