From 75edec60570eff714f0e07b4ca173ed94e2486f6 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Tue, 5 Mar 2024 11:18:33 +0100 Subject: [PATCH 01/18] Make frontend component --- .../javascripts/components/download_button.ts | 90 +++++++++++++++++++ app/javascript/packs/export.js | 1 + .../exports/download_submissions.html.erb | 2 +- 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/components/download_button.ts diff --git a/app/assets/javascripts/components/download_button.ts b/app/assets/javascripts/components/download_button.ts new file mode 100644 index 0000000000..f2ca0d5636 --- /dev/null +++ b/app/assets/javascripts/components/download_button.ts @@ -0,0 +1,90 @@ +import { DodonaElement } from "components/meta/dodona_element"; +import { html, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators.js"; + + +export type DownloadResponse = { + submissions: number; + statusUrl: string; + downloadUrl: string; +} + +export type StatusResponse = { + ready: boolean; +} + +/** + * This component represents a download button. + * It should be used within a form. + * + * @element d-download-button + */ +@customElement("d-download-button") +export class DownloadButton extends DodonaElement { + downloadResponse: DownloadResponse; + @property({ state: true }) + ready = false; + + private get form(): HTMLFormElement { + return this.closest("form") as HTMLFormElement; + } + + private get started(): boolean { + return this.downloadResponse !== undefined; + } + + private get duration(): string { + if (!this.started || this.ready) { + return ""; + } + + if (this.downloadResponse.submissions < 1000) { + return "This could take couple of seconds."; + } else if (this.downloadResponse.submissions < 10000) { + return "This could take a couple of minutes, you will receive a notification when the download is ready."; + } else { + return "This could take a while, you will receive a notification when the download is ready."; + } + } + + private async prepareDownload(): Promise { + const response = await fetch(this.form.action, { + method: this.form.method, + body: new FormData(this.form) + }); + this.downloadResponse = await response.json(); + this.tryDownload(); + } + + private async tryDownload(): Promise { + if (!this.started || this.ready) { + return; + } + + const response = await fetch(this.downloadResponse.statusUrl); + const data = await response.json(); + if (data.ready) { + window.location.href = data.downloadResponse; + this.ready = true; + } else { + setTimeout(() => this.tryDownload(), 1000); + } + } + + render(): TemplateResult { + if (!this.started) { + return html` + + `; + } else if (this.ready) { + return html` + Download again + `; + } else { + return html` + +

Preparing ${this.downloadResponse.submissions} submissions for download. ${this.duration}

+ `; + } + } +} diff --git a/app/javascript/packs/export.js b/app/javascript/packs/export.js index 69e75482b0..00fdd0708b 100644 --- a/app/javascript/packs/export.js +++ b/app/javascript/packs/export.js @@ -1,3 +1,4 @@ import { initSelection } from "export.ts"; +import "components/download_button"; window.dodona.initSelection = initSelection; diff --git a/app/views/exports/download_submissions.html.erb b/app/views/exports/download_submissions.html.erb index 03aef7dc9b..db86460e14 100644 --- a/app/views/exports/download_submissions.html.erb +++ b/app/views/exports/download_submissions.html.erb @@ -139,7 +139,7 @@ <% end %>
- +
From ea27a238254319d91627be115dda6566b71ec8fd Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Tue, 5 Mar 2024 14:13:22 +0100 Subject: [PATCH 02/18] Fix direct download --- .../javascripts/components/download_button.ts | 56 ++++++++----------- .../components/search/loading_bar.ts | 9 ++- app/controllers/exports_controller.rb | 10 +++- app/views/exports/show.json.jbuilder | 4 ++ app/views/layouts/_searchbar.html.erb | 2 +- app/views/pages/home.html.erb | 2 +- config/routes.rb | 2 +- 7 files changed, 44 insertions(+), 41 deletions(-) create mode 100644 app/views/exports/show.json.jbuilder diff --git a/app/assets/javascripts/components/download_button.ts b/app/assets/javascripts/components/download_button.ts index f2ca0d5636..479ae91307 100644 --- a/app/assets/javascripts/components/download_button.ts +++ b/app/assets/javascripts/components/download_button.ts @@ -1,17 +1,7 @@ import { DodonaElement } from "components/meta/dodona_element"; import { html, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; - - -export type DownloadResponse = { - submissions: number; - statusUrl: string; - downloadUrl: string; -} - -export type StatusResponse = { - ready: boolean; -} +import "components/search/loading_bar"; /** * This component represents a download button. @@ -21,38 +11,33 @@ export type StatusResponse = { */ @customElement("d-download-button") export class DownloadButton extends DodonaElement { - downloadResponse: DownloadResponse; @property({ state: true }) ready = false; + @property({ state: true }) + exportUrl: string | undefined = undefined; private get form(): HTMLFormElement { - return this.closest("form") as HTMLFormElement; + return document.querySelector("#download_submissions") as HTMLFormElement; } private get started(): boolean { - return this.downloadResponse !== undefined; - } - - private get duration(): string { - if (!this.started || this.ready) { - return ""; - } - - if (this.downloadResponse.submissions < 1000) { - return "This could take couple of seconds."; - } else if (this.downloadResponse.submissions < 10000) { - return "This could take a couple of minutes, you will receive a notification when the download is ready."; - } else { - return "This could take a while, you will receive a notification when the download is ready."; - } + return this.exportUrl !== undefined; } private async prepareDownload(): Promise { + const data = new FormData(this.form); + // disable the form + this.form.querySelectorAll("input, button") + .forEach(e => e.setAttribute("disabled", "true")); const response = await fetch(this.form.action, { method: this.form.method, - body: new FormData(this.form) + body: data, + headers: { + "Accept": "application/json" + } }); - this.downloadResponse = await response.json(); + const json = await response.json(); + this.exportUrl = json.url; this.tryDownload(); } @@ -61,10 +46,10 @@ export class DownloadButton extends DodonaElement { return; } - const response = await fetch(this.downloadResponse.statusUrl); + const response = await fetch(this.exportUrl); const data = await response.json(); if (data.ready) { - window.location.href = data.downloadResponse; + window.location.href = data.url; this.ready = true; } else { setTimeout(() => this.tryDownload(), 1000); @@ -78,12 +63,15 @@ export class DownloadButton extends DodonaElement { `; } else if (this.ready) { return html` - Download again + `; } else { return html` -

Preparing ${this.downloadResponse.submissions} submissions for download. ${this.duration}

+

+ Preparing submissions for download, this might take a couple of minutes. + Do not close this page. +

`; } } diff --git a/app/assets/javascripts/components/search/loading_bar.ts b/app/assets/javascripts/components/search/loading_bar.ts index 208f05fac3..a2f70f09de 100644 --- a/app/assets/javascripts/components/search/loading_bar.ts +++ b/app/assets/javascripts/components/search/loading_bar.ts @@ -11,12 +11,17 @@ import { DodonaElement } from "components/meta/dodona_element"; */ @customElement("d-loading-bar") export class LoadingBar extends DodonaElement { - @property({ type: Boolean, state: true }) + @property({ type: Boolean, attribute: "search-based" }) + searchBased = false; + + @property({ type: Boolean }) loading = false; constructor() { super(); - search.loadingBars.push(this); + if (this.searchBased) { + search.loadingBars.push(this); + } } show(): void { diff --git a/app/controllers/exports_controller.rb b/app/controllers/exports_controller.rb index da839c71d2..988f97c179 100644 --- a/app/controllers/exports_controller.rb +++ b/app/controllers/exports_controller.rb @@ -11,6 +11,11 @@ def index @exports = policy_scope(Export) end + def show + @export = Export.find(params[:id]) + authorize @export + end + def new_series_export @series = Series.find(params[:id]) authorize @series, :export? @@ -89,13 +94,14 @@ def create(item, list) # Only retain supported options from the received function parameters options = params.permit(Export::Zipper::SUPPORTED_OPTIONS) - Export.create(user: current_user).delay(queue: 'exports').start(item, list, ([@user] if @user), options) + export = Export.create(user: current_user) + export.delay(queue: 'exports').start(item, list, ([@user] if @user), options) respond_to do |format| format.html do flash[:notice] = I18n.t('exports.index.export_started') redirect_to action: 'index' end - format.json { head :accepted } + format.json { render json: { url: export_path(export) } } end end end diff --git a/app/views/exports/show.json.jbuilder b/app/views/exports/show.json.jbuilder new file mode 100644 index 0000000000..5346a8bcb6 --- /dev/null +++ b/app/views/exports/show.json.jbuilder @@ -0,0 +1,4 @@ +json.ready @export.finished? +if @export.finished? && @export.archive.attached? + json.url rails_blob_path(@export.archive, disposition: "attachment") +end diff --git a/app/views/layouts/_searchbar.html.erb b/app/views/layouts/_searchbar.html.erb index 5a16f25173..9765d8c0b4 100644 --- a/app/views/layouts/_searchbar.html.erb +++ b/app/views/layouts/_searchbar.html.erb @@ -7,7 +7,7 @@ <% if local_assigns[:actions]&.any? %> diff --git a/app/views/pages/home.html.erb b/app/views/pages/home.html.erb index 0b96f9c23f..9f608d88d1 100644 --- a/app/views/pages/home.html.erb +++ b/app/views/pages/home.html.erb @@ -28,7 +28,7 @@ default="<%= @year %>"> - +
<%= render partial: "course_cards", locals: {courses: @courses} %> diff --git a/config/routes.rb b/config/routes.rb index a151851658..7d8d9b2493 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -79,7 +79,7 @@ end end - resources :exports, except: %i[show edit update new destroy create] do + resources :exports, except: %i[edit update new destroy create] do get 'users/:id', on: :collection, to: 'exports#new_user_export', as: 'users' post 'users/:id', on: :collection, to: 'exports#create_user_export' get 'courses/:id', on: :collection, to: 'exports#new_course_export', as: 'courses' From 78babf1fc284a807c39a080ed38d589eecb3d288 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Tue, 5 Mar 2024 14:22:44 +0100 Subject: [PATCH 03/18] remove index page --- app/controllers/exports_controller.rb | 21 +++++---- app/helpers/notifications_helper.rb | 4 +- app/views/exports/index.html.erb | 63 --------------------------- 3 files changed, 12 insertions(+), 76 deletions(-) delete mode 100644 app/views/exports/index.html.erb diff --git a/app/controllers/exports_controller.rb b/app/controllers/exports_controller.rb index 988f97c179..14c20295b2 100644 --- a/app/controllers/exports_controller.rb +++ b/app/controllers/exports_controller.rb @@ -4,16 +4,19 @@ class ExportsController < ApplicationController before_action :set_user, only: %i[new_series_export new_course_export create_series_export create_course_export] - def index - authorize Export - @title = I18n.t('exports.index.title') - @highlighted_id = (params[:highlighted] || 0).to_i - @exports = policy_scope(Export) - end - def show @export = Export.find(params[:id]) authorize @export + respond_to do |format| + format.json + format.any(:zip, :html) do + if @export.finished? && @export.archive.attached? + redirect_to rails_blob_path(@export.archive, disposition: "attachment") + else + head :not_found + end + end + end end def new_series_export @@ -97,10 +100,6 @@ def create(item, list) export = Export.create(user: current_user) export.delay(queue: 'exports').start(item, list, ([@user] if @user), options) respond_to do |format| - format.html do - flash[:notice] = I18n.t('exports.index.export_started') - redirect_to action: 'index' - end format.json { render json: { url: export_path(export) } } end end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index cd9d6c0d4f..8c97d2cdee 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -1,13 +1,13 @@ module NotificationsHelper def base_notifiable_url_params(notification) - return { controller: 'exports', action: 'index' } if notification.notifiable_type == 'Export' + return { controller: 'exports', action: 'show', id: notification.notifiable_id } if notification.notifiable_type == 'Export' return { controller: 'submissions', action: 'show', id: notification.notifiable_id } if notification.notifiable_type == 'Submission' { controller: 'evaluations', action: 'overview', id: notification.notifiable_id } if notification.notifiable_type == 'Evaluation' end def notifiable_url(notification) - return exports_path(highlighted: notification.notifiable_id) if notification.notifiable_type == 'Export' + return export_path(notification.notifiable_id) if notification.notifiable_type == 'Export' return submission_path(notification.notifiable_id, anchor: 'code') if notification.notifiable_type == 'Submission' overview_evaluation_path(notification.notifiable_id) if notification.notifiable_type == 'Evaluation' diff --git a/app/views/exports/index.html.erb b/app/views/exports/index.html.erb deleted file mode 100644 index 742d6d7025..0000000000 --- a/app/views/exports/index.html.erb +++ /dev/null @@ -1,63 +0,0 @@ -
-
-
-
-

<%= t ".title" %>

-
-
- - <%= t '.explanation' %> - - <% if @exports.any? %> -
- - - - - - - - - - - <% @exports.each do |export| %> - > - - - - - - <% end %> - -
<%= t '.status' %><%= t '.started' %><%= t '.filename' %>
<%= Export.human_enum_name(:status, export.status) %><%= t('.created', when: time_ago_in_words(export.created_at)) %> - <% if export.archive.attached? %> - <%= export.archive.filename %> - <% end %> - - <% if export.archive.attached? %> - <%= link_to rails_blob_path(export.archive, disposition: "attachment"), class: 'btn btn-icon' do %> - - <% end %> - <% end %> -
- - - <%= t('.automatic_deletion') %> - -
- <% else %> - - <%= t '.no_exports_yet_html', user_link: users_exports_path(current_user) %> - - <% end %> -
-
-
-
-<% if @exports.where(status: :started).any? %> - -<% end %> From 85d47052df980eaecabe04903e1105421dedbf62 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Tue, 5 Mar 2024 14:56:11 +0100 Subject: [PATCH 04/18] Fix translations --- .../javascripts/components/download_button.ts | 22 ++++++++++--------- app/assets/javascripts/i18n/translations.json | 10 +++++++++ config/locales/js/en.yml | 5 +++++ config/locales/js/nl.yml | 4 ++++ 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/components/download_button.ts b/app/assets/javascripts/components/download_button.ts index 479ae91307..bc24e2d435 100644 --- a/app/assets/javascripts/components/download_button.ts +++ b/app/assets/javascripts/components/download_button.ts @@ -2,6 +2,7 @@ import { DodonaElement } from "components/meta/dodona_element"; import { html, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; import "components/search/loading_bar"; +import { i18n } from "i18n/i18n"; /** * This component represents a download button. @@ -14,14 +15,14 @@ export class DownloadButton extends DodonaElement { @property({ state: true }) ready = false; @property({ state: true }) - exportUrl: string | undefined = undefined; + url: string | undefined = undefined; private get form(): HTMLFormElement { return document.querySelector("#download_submissions") as HTMLFormElement; } private get started(): boolean { - return this.exportUrl !== undefined; + return this.url !== undefined; } private async prepareDownload(): Promise { @@ -37,7 +38,7 @@ export class DownloadButton extends DodonaElement { } }); const json = await response.json(); - this.exportUrl = json.url; + this.url = json.url; this.tryDownload(); } @@ -46,7 +47,7 @@ export class DownloadButton extends DodonaElement { return; } - const response = await fetch(this.exportUrl); + const response = await fetch(this.url); const data = await response.json(); if (data.ready) { window.location.href = data.url; @@ -59,19 +60,20 @@ export class DownloadButton extends DodonaElement { render(): TemplateResult { if (!this.started) { return html` - + `; } else if (this.ready) { return html` - + `; } else { return html` -

- Preparing submissions for download, this might take a couple of minutes. - Do not close this page. -

+

${i18n.t("js.download_button.downloading")}

`; } } diff --git a/app/assets/javascripts/i18n/translations.json b/app/assets/javascripts/i18n/translations.json index 75631ffce4..46b72fcf7a 100644 --- a/app/assets/javascripts/i18n/translations.json +++ b/app/assets/javascripts/i18n/translations.json @@ -187,6 +187,11 @@ "date_before": "before", "date_on": "on", "description_languages": "Language of the description", + "download_button": { + "done": "Downloaded, go back", + "download": "Download", + "downloading": "Preparing submissions for download, this might take a couple of minutes. Do not close this page." + }, "dropdown": { "multi": { "activity_types": "Activity types", @@ -712,6 +717,11 @@ "date_before": "voor", "date_on": "op", "description_languages": "Taal van de beschrijving", + "download_button": { + "done": "Gedownload, ga terug", + "download": "Downloaden", + "downloading": "Bezig met het voorbereiden van de download, dit kan enkele minuten duren. Sluit dit deze pagina niet." + }, "dropdown": { "multi": { "activity_types": "Activiteitstypes", diff --git a/config/locales/js/en.yml b/config/locales/js/en.yml index d4bab5d56f..e53dec2fc0 100644 --- a/config/locales/js/en.yml +++ b/config/locales/js/en.yml @@ -327,3 +327,8 @@ en: feedbacks: submission: submit: Submit this solution + download_button: + download: Download + done: Downloaded, go back + downloading: Preparing submissions for download, this might take a couple of minutes. Do not close this page. + diff --git a/config/locales/js/nl.yml b/config/locales/js/nl.yml index 81d7882eb9..3cf05abed4 100644 --- a/config/locales/js/nl.yml +++ b/config/locales/js/nl.yml @@ -327,3 +327,7 @@ nl: feedbacks: submission: submit: Deze oplossing indienen + download_button: + download: Downloaden + done: Gedownload, ga terug + downloading: Bezig met het voorbereiden van de download, dit kan enkele minuten duren. Sluit dit deze pagina niet. From fa6ca3e0505eeb2f0a40e166ce6ddcedf5039975 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Tue, 5 Mar 2024 15:13:19 +0100 Subject: [PATCH 05/18] Remove notification --- app/helpers/notifications_helper.rb | 3 --- app/models/export.rb | 2 -- db/migrate/20240305141148_remove_export_notifications.rb | 5 +++++ db/schema.rb | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 db/migrate/20240305141148_remove_export_notifications.rb diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 8c97d2cdee..b60d2976a8 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -1,20 +1,17 @@ module NotificationsHelper def base_notifiable_url_params(notification) - return { controller: 'exports', action: 'show', id: notification.notifiable_id } if notification.notifiable_type == 'Export' return { controller: 'submissions', action: 'show', id: notification.notifiable_id } if notification.notifiable_type == 'Submission' { controller: 'evaluations', action: 'overview', id: notification.notifiable_id } if notification.notifiable_type == 'Evaluation' end def notifiable_url(notification) - return export_path(notification.notifiable_id) if notification.notifiable_type == 'Export' return submission_path(notification.notifiable_id, anchor: 'code') if notification.notifiable_type == 'Submission' overview_evaluation_path(notification.notifiable_id) if notification.notifiable_type == 'Evaluation' end def notifiable_icon(notification) - return 'mdi-file-download-outline' if notification.notifiable_type == 'Export' return 'mdi-comment-account-outline' if notification.notifiable_type == 'Submission' 'mdi-comment-multiple-outline' if notification.notifiable_type == 'Evaluation' diff --git a/app/models/export.rb b/app/models/export.rb index 97a78b469f..55eaa3c5f3 100644 --- a/app/models/export.rb +++ b/app/models/export.rb @@ -15,7 +15,6 @@ class Export < ApplicationRecord AUTOMATICALLY_DELETE_AFTER = 30.days belongs_to :user - has_one :notification, as: :notifiable, dependent: :destroy has_one_attached :archive enum status: { started: 0, finished: 1, failed: 2 } @@ -38,7 +37,6 @@ def start(item, list, users, params) ) delay(queue: 'cleaning', run_at: AUTOMATICALLY_DELETE_AFTER.from_now).destroy - notification = Notification.new(user: user, message: 'exports.index.ready_for_download') update(status: :finished, notification: notification) end end diff --git a/db/migrate/20240305141148_remove_export_notifications.rb b/db/migrate/20240305141148_remove_export_notifications.rb new file mode 100644 index 0000000000..8300a79870 --- /dev/null +++ b/db/migrate/20240305141148_remove_export_notifications.rb @@ -0,0 +1,5 @@ +class RemoveExportNotifications < ActiveRecord::Migration[7.1] + def change + Notification.where(notifiable_type: 'Export').destroy_all + end +end diff --git a/db/schema.rb b/db/schema.rb index 61ed82a9c9..a49ca4d527 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_02_26_090515) do +ActiveRecord::Schema[7.1].define(version: 2024_03_05_141148) do create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false From 84f58613e2321def68e0fb4d8f102e2ad7fb0b80 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Tue, 5 Mar 2024 15:14:30 +0100 Subject: [PATCH 06/18] Speed up clean up --- app/models/export.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/export.rb b/app/models/export.rb index 55eaa3c5f3..9f6445fdb4 100644 --- a/app/models/export.rb +++ b/app/models/export.rb @@ -12,7 +12,7 @@ class Export < ApplicationRecord include ExportHelper - AUTOMATICALLY_DELETE_AFTER = 30.days + AUTOMATICALLY_DELETE_AFTER = 1.day belongs_to :user has_one_attached :archive From 04d0bc9c539f9b934322f8cd35ef1d867fa684a0 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Tue, 5 Mar 2024 15:36:02 +0100 Subject: [PATCH 07/18] Remove mention of notification --- app/models/export.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/export.rb b/app/models/export.rb index 9f6445fdb4..d616e16079 100644 --- a/app/models/export.rb +++ b/app/models/export.rb @@ -37,6 +37,6 @@ def start(item, list, users, params) ) delay(queue: 'cleaning', run_at: AUTOMATICALLY_DELETE_AFTER.from_now).destroy - update(status: :finished, notification: notification) + update(status: :finished) end end From 100cbd6f81bc79e9a124138517682b45cef22273 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Tue, 5 Mar 2024 15:39:24 +0100 Subject: [PATCH 08/18] Fix linting --- app/controllers/exports_controller.rb | 2 +- app/views/exports/show.json.jbuilder | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/controllers/exports_controller.rb b/app/controllers/exports_controller.rb index 14c20295b2..5096674153 100644 --- a/app/controllers/exports_controller.rb +++ b/app/controllers/exports_controller.rb @@ -11,7 +11,7 @@ def show format.json format.any(:zip, :html) do if @export.finished? && @export.archive.attached? - redirect_to rails_blob_path(@export.archive, disposition: "attachment") + redirect_to rails_blob_path(@export.archive, disposition: 'attachment') else head :not_found end diff --git a/app/views/exports/show.json.jbuilder b/app/views/exports/show.json.jbuilder index 5346a8bcb6..cb5a8349e5 100644 --- a/app/views/exports/show.json.jbuilder +++ b/app/views/exports/show.json.jbuilder @@ -1,4 +1,2 @@ json.ready @export.finished? -if @export.finished? && @export.archive.attached? - json.url rails_blob_path(@export.archive, disposition: "attachment") -end +json.url rails_blob_path(@export.archive, disposition: 'attachment') if @export.finished? && @export.archive.attached? From 7ab9eb7977b73235192a1b9b6c0f317c2eb98736 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Tue, 5 Mar 2024 16:22:37 +0100 Subject: [PATCH 09/18] Fix tests --- test/controllers/exports_controller_test.rb | 73 +++++++++++---------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/test/controllers/exports_controller_test.rb b/test/controllers/exports_controller_test.rb index c58cd5abb2..fceae42352 100644 --- a/test/controllers/exports_controller_test.rb +++ b/test/controllers/exports_controller_test.rb @@ -25,39 +25,32 @@ class ExportsControllerTest < ActionDispatch::IntegrationTest end test 'should download only last submissions' do - post series_exports_path(@series), params: { all: true, only_last_submission: true, with_info: true } + post series_exports_path(@series), params: { all: true, only_last_submission: true, with_info: true, format: :json } - assert_redirected_to exports_path + assert_response :success count = @students.map { |u| @series.exercises.map { |e| e.last_submission(u, @series) } }.flatten.select(&:present?).count assert_zip ActiveStorage::Blob.last.download, with_info: true, solution_count: count, data: @data end - test 'should create notification' do - assert_difference('Notification.count', 1) do - post series_exports_path(@series), params: { all: true, only_last_submission: true, with_info: true } - end - assert_redirected_to exports_path - end - test 'should be grouped by user' do - post series_exports_path(@series), params: { all: true, group_by: 'user' } + post series_exports_path(@series), params: { all: true, group_by: 'user', format: :json } - assert_redirected_to exports_path + assert_response :success assert_zip ActiveStorage::Blob.last.download, group_by: 'user', data: @data end test 'should be grouped by exercise' do - post series_exports_path(@series), params: { all: true, group_by: 'exercise' } + post series_exports_path(@series), params: { all: true, group_by: 'exercise', format: :json } - assert_redirected_to exports_path + assert_response :success assert_zip ActiveStorage::Blob.last.download, group_by: 'exercise', data: @data end test 'should retrieve all submissions' do - post series_exports_path(@series), params: { all: true } + post series_exports_path(@series), params: { all: true, format: :json } - assert_redirected_to exports_path + assert_response :success assert_zip ActiveStorage::Blob.last.download, solution_count: Submission.count, data: @data end @@ -73,25 +66,25 @@ class ExportsControllerTest < ActionDispatch::IntegrationTest end end.flatten.length - post series_exports_path(@series), params: { all: true, all_students: true } + post series_exports_path(@series), params: { all: true, all_students: true, format: :json } - assert_redirected_to exports_path + assert_response :success assert_zip ActiveStorage::Blob.last.download, solution_count: zip_submission_count, data: @data end test 'zip should only contain submissions before deadline' do @series.update(deadline: 1.year.ago) - post series_exports_path(@series), params: { all: true, deadline: true } + post series_exports_path(@series), params: { all: true, deadline: true, format: :json } - assert_redirected_to exports_path + assert_response :success zip_submission_count = @series.exercises.map { |ex| ex.submissions.before_deadline(@series.deadline) }.flatten.length assert_zip ActiveStorage::Blob.last.download, solution_count: zip_submission_count, data: @data @series.update(deadline: 2.years.from_now) - post series_exports_path(@series), params: { all: true, deadline: true } + post series_exports_path(@series), params: { all: true, deadline: true, format: :json } - assert_redirected_to exports_path + assert_response :success zip_submission_count = @series.exercises.map { |ex| ex.submissions.before_deadline(@series.deadline) }.flatten.length assert_zip ActiveStorage::Blob.last.download, solution_count: zip_submission_count, data: @data @@ -99,7 +92,7 @@ class ExportsControllerTest < ActionDispatch::IntegrationTest test 'should only download from specific exercises' do sample_exercises = @series.exercises.sample(3) - post series_exports_path(@series), params: { selected_ids: sample_exercises.map(&:id), all_students: true } + post series_exports_path(@series), params: { selected_ids: sample_exercises.map(&:id), all_students: true, format: :json } zip_submission_count = @data[:users].map do |u| sample_exercises.map do |ex| subs = ex.submissions.of_user(u).in_course(@series.course) @@ -121,7 +114,8 @@ class ExportsControllerTest < ActionDispatch::IntegrationTest course: @series.course, with_info: true, group_by: 'exercise', - data: @data } + data: @data, + format: :json } options[:solution_count] = @data[:users].map do |u| sample_exercises.map do |ex| ex.submissions.of_user(u).in_course(@series.course).before_deadline(@series.deadline).limit(1).first @@ -138,11 +132,12 @@ class ExportsControllerTest < ActionDispatch::IntegrationTest only_last_submission: false, data: @data, solution_count: Submission.all.in_course(@course).count, - all: true + all: true, + format: :json } post courses_exports_path(@course), params: options - assert_redirected_to exports_path + assert_response :success options[:group_by] = 'series' assert_zip ActiveStorage::Blob.last.download, options @@ -158,11 +153,12 @@ class ExportsControllerTest < ActionDispatch::IntegrationTest only_last_submission: false, data: @data, solution_count: Submission.all.in_course(@course).count, - all: true + all: true, + format: :json } post courses_exports_path(@course), params: options - assert_redirected_to exports_path + assert_response :success options[:group_by] = 'series' assert_zip ActiveStorage::Blob.last.download, options @@ -180,12 +176,13 @@ class ExportsControllerTest < ActionDispatch::IntegrationTest only_last_submission: false, data: @data, solution_count: Submission.all.in_course(@course).count, - all: true + all: true, + format: :json } sign_in u post courses_exports_path(@course), params: options - assert_redirected_to exports_path + assert_response :success options[:group_by] = 'series' assert_zip ActiveStorage::Blob.last.download, options @@ -200,11 +197,12 @@ class ExportsControllerTest < ActionDispatch::IntegrationTest with_info: true, data: @data, all: true, - solution_count: @course.users.count * @course.series.map(&:exercises).flatten.count + solution_count: @course.users.count * @course.series.map(&:exercises).flatten.count, + format: :json } post courses_exports_path(@course), params: options - assert_redirected_to exports_path + assert_response :success assert_zip ActiveStorage::Blob.last.download, options end @@ -213,7 +211,8 @@ class ExportsControllerTest < ActionDispatch::IntegrationTest group_by: 'exercise', all_students: 'true', data: @data, - all: true + all: true, + format: :json } options[:solution_count] = @course.series.map do |series| @course.users.map do |user| @@ -233,7 +232,8 @@ class ExportsControllerTest < ActionDispatch::IntegrationTest options = { data: @data, all: true, - solution_count: Submission.all.of_user(student).count + solution_count: Submission.all.of_user(student).count, + format: :json } @data[:user] = student post users_exports_path(student), params: options @@ -248,7 +248,8 @@ class ExportsControllerTest < ActionDispatch::IntegrationTest options = { data: @data, all: true, - solution_count: Submission.all.of_user(other_student).count + solution_count: Submission.all.of_user(other_student).count, + format: :json } post users_exports_path(other_student, format: :json), params: options @@ -265,7 +266,7 @@ class ExportsControllerTest < ActionDispatch::IntegrationTest post courses_exports_path(@course, user_id: @students[0].id, format: :json) - assert_response :accepted + assert_response :success end test 'should not be able to export course if not course member' do @@ -290,7 +291,7 @@ class ExportsControllerTest < ActionDispatch::IntegrationTest post series_exports_path(@course.series.first, user_id: @students[0].id, format: :json) - assert_response :accepted + assert_response :success end test 'should not be able to export series if not course member' do From 0276b0abac144160b8791305880fb2a2f4b68822 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Wed, 6 Mar 2024 11:37:36 +0100 Subject: [PATCH 10/18] Update config/locales/js/nl.yml Co-authored-by: Niko Strijbol --- config/locales/js/nl.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/js/nl.yml b/config/locales/js/nl.yml index 3cf05abed4..e8245444c5 100644 --- a/config/locales/js/nl.yml +++ b/config/locales/js/nl.yml @@ -330,4 +330,4 @@ nl: download_button: download: Downloaden done: Gedownload, ga terug - downloading: Bezig met het voorbereiden van de download, dit kan enkele minuten duren. Sluit dit deze pagina niet. + downloading: Bezig met het voorbereiden van de download, dit kan enkele minuten duren. Sluit deze pagina niet. From 2e09eb429701f264f1cfa48df4e8d944df547b45 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Wed, 6 Mar 2024 11:41:55 +0100 Subject: [PATCH 11/18] Generalise methods for dolos --- .../javascripts/components/download_button.ts | 33 ++++-------------- app/assets/javascripts/export.ts | 34 ++++++++++++++++++- app/assets/javascripts/i18n/translations.json | 2 +- 3 files changed, 40 insertions(+), 29 deletions(-) diff --git a/app/assets/javascripts/components/download_button.ts b/app/assets/javascripts/components/download_button.ts index bc24e2d435..194f9288b3 100644 --- a/app/assets/javascripts/components/download_button.ts +++ b/app/assets/javascripts/components/download_button.ts @@ -3,6 +3,7 @@ import { html, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; import "components/search/loading_bar"; import { i18n } from "i18n/i18n"; +import { exportLocation, prepareExport } from "export"; /** * This component represents a download button. @@ -25,42 +26,20 @@ export class DownloadButton extends DodonaElement { return this.url !== undefined; } - private async prepareDownload(): Promise { + private async download(): Promise { const data = new FormData(this.form); // disable the form this.form.querySelectorAll("input, button") .forEach(e => e.setAttribute("disabled", "true")); - const response = await fetch(this.form.action, { - method: this.form.method, - body: data, - headers: { - "Accept": "application/json" - } - }); - const json = await response.json(); - this.url = json.url; - this.tryDownload(); - } - - private async tryDownload(): Promise { - if (!this.started || this.ready) { - return; - } - - const response = await fetch(this.url); - const data = await response.json(); - if (data.ready) { - window.location.href = data.url; - this.ready = true; - } else { - setTimeout(() => this.tryDownload(), 1000); - } + this.url = await prepareExport(this.form.action, data); + window.location.href = await exportLocation(this.url); + this.ready = true; } render(): TemplateResult { if (!this.started) { return html` - `; diff --git a/app/assets/javascripts/export.ts b/app/assets/javascripts/export.ts index b41076f466..b6538bc590 100644 --- a/app/assets/javascripts/export.ts +++ b/app/assets/javascripts/export.ts @@ -84,5 +84,37 @@ function initSelection(): void { init(); } -export { initSelection }; +/** + * Prepare the download of a file by sending a POST request to the server. + * Returns the URL of the metadata for the file to download. + * @param url The URL of the export endpoint + * @param data The export settings to send to the server + */ +async function prepareExport(url: string, data: FormData): Promise { + const response = await fetch(url, { + method: "POST", + body: data, + headers: { + "Accept": "application/json" + } + }); + const json = await response.json(); + return json.url; +} + +/** + * Returns the url of the blob to download when the download is ready. + * @param url The URL of the download endpoint + */ +async function exportLocation(url: string): Promise { + const response = await fetch(url); + const data = await response.json(); + if (!data.ready) { + await new Promise(resolve => setTimeout(resolve, 1000)); + return await exportLocation(url); + } + return data.url; +} + +export { initSelection, prepareExport, exportLocation }; diff --git a/app/assets/javascripts/i18n/translations.json b/app/assets/javascripts/i18n/translations.json index 46b72fcf7a..5d3c224f5d 100644 --- a/app/assets/javascripts/i18n/translations.json +++ b/app/assets/javascripts/i18n/translations.json @@ -720,7 +720,7 @@ "download_button": { "done": "Gedownload, ga terug", "download": "Downloaden", - "downloading": "Bezig met het voorbereiden van de download, dit kan enkele minuten duren. Sluit dit deze pagina niet." + "downloading": "Bezig met het voorbereiden van de download, dit kan enkele minuten duren. Sluit deze pagina niet." }, "dropdown": { "multi": { From 97da8b0f4f407b29541391d1ba72b540e7581d72 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Wed, 6 Mar 2024 11:55:36 +0100 Subject: [PATCH 12/18] Add retry button --- .../javascripts/components/download_button.ts | 24 +++++++++++++------ app/assets/javascripts/i18n/translations.json | 14 +++++++---- config/locales/js/en.yml | 4 +++- config/locales/js/nl.yml | 6 +++-- 4 files changed, 33 insertions(+), 15 deletions(-) diff --git a/app/assets/javascripts/components/download_button.ts b/app/assets/javascripts/components/download_button.ts index 194f9288b3..4c5d57e4c7 100644 --- a/app/assets/javascripts/components/download_button.ts +++ b/app/assets/javascripts/components/download_button.ts @@ -14,16 +14,20 @@ import { exportLocation, prepareExport } from "export"; @customElement("d-download-button") export class DownloadButton extends DodonaElement { @property({ state: true }) - ready = false; + exportDataUrl: string | undefined = undefined; @property({ state: true }) - url: string | undefined = undefined; + downloadUrl: string | undefined = undefined; private get form(): HTMLFormElement { return document.querySelector("#download_submissions") as HTMLFormElement; } private get started(): boolean { - return this.url !== undefined; + return this.exportDataUrl !== undefined; + } + + private get ready(): boolean { + return this.downloadUrl !== undefined; } private async download(): Promise { @@ -31,9 +35,9 @@ export class DownloadButton extends DodonaElement { // disable the form this.form.querySelectorAll("input, button") .forEach(e => e.setAttribute("disabled", "true")); - this.url = await prepareExport(this.form.action, data); - window.location.href = await exportLocation(this.url); - this.ready = true; + this.exportDataUrl = await prepareExport(this.form.action, data); + this.downloadUrl = await exportLocation(this.exportDataUrl); + window.location.href = this.downloadUrl; } render(): TemplateResult { @@ -45,8 +49,14 @@ export class DownloadButton extends DodonaElement { `; } else if (this.ready) { return html` +

+ ${i18n.t("js.download_button.ready")} +

+ + ${i18n.t("js.download_button.retry")} + `; } else { diff --git a/app/assets/javascripts/i18n/translations.json b/app/assets/javascripts/i18n/translations.json index 5d3c224f5d..b59aa7691a 100644 --- a/app/assets/javascripts/i18n/translations.json +++ b/app/assets/javascripts/i18n/translations.json @@ -188,9 +188,11 @@ "date_on": "on", "description_languages": "Language of the description", "download_button": { - "done": "Downloaded, go back", "download": "Download", - "downloading": "Preparing submissions for download, this might take a couple of minutes. Do not close this page." + "downloading": "Preparing submissions for download, this might take a couple of minutes. Do not close this page.", + "go_back": "Go back", + "ready": "Export is ready. The download should start automatically.", + "retry": "Retry" }, "dropdown": { "multi": { @@ -718,9 +720,11 @@ "date_on": "op", "description_languages": "Taal van de beschrijving", "download_button": { - "done": "Gedownload, ga terug", - "download": "Downloaden", - "downloading": "Bezig met het voorbereiden van de download, dit kan enkele minuten duren. Sluit deze pagina niet." + "download": "Download", + "downloading": "Bezig met het voorbereiden van de download, dit kan enkele minuten duren. Sluit deze pagina niet.", + "go_back": "Keer terug", + "ready": "De export is klaar. De download start automatisch.", + "retry": "Probeer opnieuw" }, "dropdown": { "multi": { diff --git a/config/locales/js/en.yml b/config/locales/js/en.yml index e53dec2fc0..1f9ff7175a 100644 --- a/config/locales/js/en.yml +++ b/config/locales/js/en.yml @@ -329,6 +329,8 @@ en: submit: Submit this solution download_button: download: Download - done: Downloaded, go back + retry: Retry + go_back: Go back + ready: Export is ready. The download should start automatically. downloading: Preparing submissions for download, this might take a couple of minutes. Do not close this page. diff --git a/config/locales/js/nl.yml b/config/locales/js/nl.yml index e8245444c5..89d4a62efb 100644 --- a/config/locales/js/nl.yml +++ b/config/locales/js/nl.yml @@ -328,6 +328,8 @@ nl: submission: submit: Deze oplossing indienen download_button: - download: Downloaden - done: Gedownload, ga terug + download: Download + retry: Probeer opnieuw + go_back: Keer terug + ready: De export is klaar. De download start automatisch. downloading: Bezig met het voorbereiden van de download, dit kan enkele minuten duren. Sluit deze pagina niet. From 596a6dc33ee3b95cbea4ff3c6ac611031fa0d9b1 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Mon, 11 Mar 2024 11:37:33 +0100 Subject: [PATCH 13/18] Make downloading an extra step --- .../javascripts/components/download_button.ts | 69 ------------------- app/assets/javascripts/export.ts | 47 +++++++++++-- app/assets/javascripts/i18n/translations.json | 16 ++--- app/javascript/packs/export.js | 2 +- .../exports/download_submissions.html.erb | 29 +++++++- config/locales/js/en.yml | 4 -- config/locales/js/nl.yml | 8 +-- config/locales/views/exports/en.yml | 4 ++ config/locales/views/exports/nl.yml | 4 ++ 9 files changed, 85 insertions(+), 98 deletions(-) delete mode 100644 app/assets/javascripts/components/download_button.ts diff --git a/app/assets/javascripts/components/download_button.ts b/app/assets/javascripts/components/download_button.ts deleted file mode 100644 index 4c5d57e4c7..0000000000 --- a/app/assets/javascripts/components/download_button.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { DodonaElement } from "components/meta/dodona_element"; -import { html, TemplateResult } from "lit"; -import { customElement, property } from "lit/decorators.js"; -import "components/search/loading_bar"; -import { i18n } from "i18n/i18n"; -import { exportLocation, prepareExport } from "export"; - -/** - * This component represents a download button. - * It should be used within a form. - * - * @element d-download-button - */ -@customElement("d-download-button") -export class DownloadButton extends DodonaElement { - @property({ state: true }) - exportDataUrl: string | undefined = undefined; - @property({ state: true }) - downloadUrl: string | undefined = undefined; - - private get form(): HTMLFormElement { - return document.querySelector("#download_submissions") as HTMLFormElement; - } - - private get started(): boolean { - return this.exportDataUrl !== undefined; - } - - private get ready(): boolean { - return this.downloadUrl !== undefined; - } - - private async download(): Promise { - const data = new FormData(this.form); - // disable the form - this.form.querySelectorAll("input, button") - .forEach(e => e.setAttribute("disabled", "true")); - this.exportDataUrl = await prepareExport(this.form.action, data); - this.downloadUrl = await exportLocation(this.exportDataUrl); - window.location.href = this.downloadUrl; - } - - render(): TemplateResult { - if (!this.started) { - return html` - - `; - } else if (this.ready) { - return html` -

- ${i18n.t("js.download_button.ready")} -

- - ${i18n.t("js.download_button.retry")} - - - `; - } else { - return html` - -

${i18n.t("js.download_button.downloading")}

- `; - } - } -} diff --git a/app/assets/javascripts/export.ts b/app/assets/javascripts/export.ts index b6538bc590..23ca4ffb03 100644 --- a/app/assets/javascripts/export.ts +++ b/app/assets/javascripts/export.ts @@ -1,4 +1,5 @@ import { i18n } from "i18n/i18n"; +import { Collapse } from "bootstrap"; function initSelection(): void { const selectAll = document.querySelector("#check-all") as HTMLInputElement; @@ -7,23 +8,34 @@ function initSelection(): void { const errorWrapper = document.querySelector("#errors-wrapper"); const choosePanel = document.querySelector("#choose-panel"); - const chooseCollapse = new bootstrap.Collapse(choosePanel.querySelector(".panel-collapse"), { toggle: false }); + const chooseCollapse = new Collapse(choosePanel.querySelector(".panel-collapse"), { toggle: false }); const chooseOptionsPanel = document.querySelector("#choose-options-panel"); const chooseOptionsElement = chooseOptionsPanel.querySelector(".panel-collapse"); - const chooseOptionsCollapse = new bootstrap.Collapse(chooseOptionsElement, { toggle: false }); + const chooseOptionsCollapse = new Collapse(chooseOptionsElement, { toggle: false }); const form = document.querySelector("#download_submissions") as HTMLFormElement; const defaultAction = form.action; + const startDownload = document.getElementById("start-download") as HTMLButtonElement; + const downloadingPanel = document.getElementById("downloading-panel") as HTMLDivElement; + const downloadingCollapse = new Collapse(downloadingPanel.querySelector(".panel-collapse"), { toggle: false }); + function init(): void { initCheckboxes(); initContinueButton(); + initDownloadButton(); choosePanel.querySelector(".panel-collapse").addEventListener("show.bs.collapse", () => { chooseOptionsCollapse.hide(); + downloadingCollapse.hide(); }); chooseOptionsPanel.querySelector(".panel-collapse").addEventListener("show.bs.collapse", () => { chooseCollapse.hide(); + downloadingCollapse.hide(); + }); + downloadingPanel.querySelector(".panel-collapse").addEventListener("show.bs.collapse", () => { + chooseCollapse.hide(); + chooseOptionsCollapse.hide(); }); } @@ -69,18 +81,44 @@ function initSelection(): void { if (formUrl) { errorWrapper.classList.add("hidden"); chooseOptionsPanel.classList.remove("hidden"); // this panel is initially hidden - chooseCollapse.hide(); chooseOptionsCollapse.show(); form.action = formUrl; } else { chooseCollapse.show(); - chooseOptionsCollapse.hide(); errorWrapper.classList.remove("hidden"); document.querySelector("#warning-message-wrapper").innerHTML = i18n.t("js.no_selection"); } })); } + function initDownloadButton(): void { + startDownload.addEventListener("click", async () => { + const data = new FormData(form); + downloadingPanel.classList.remove("hidden"); + downloadingCollapse.show(); + + // disable the collapse steppers + document.querySelectorAll(".panel-heading a").forEach(a => { + a.classList.add("disabled"); + a.attributes.removeNamedItem("data-bs-toggle"); + }); + + const exportDataUrl = await prepareExport(form.action, data); + const downloadUrl = await exportLocation(exportDataUrl); + + // Update the stepper content + downloadingPanel.querySelector(".stepper-part").replaceChildren( + document.createTextNode(i18n.t("js.export.ready")) + ); + // Update the retry download button + const retryButton = document.getElementById("retry-download-button") as HTMLAnchorElement; + retryButton.href = downloadUrl; + retryButton.attributes.removeNamedItem("disabled"); + + window.location.href = downloadUrl; + }); + } + init(); } @@ -117,4 +155,3 @@ async function exportLocation(url: string): Promise { } export { initSelection, prepareExport, exportLocation }; - diff --git a/app/assets/javascripts/i18n/translations.json b/app/assets/javascripts/i18n/translations.json index b59aa7691a..4f24e03ede 100644 --- a/app/assets/javascripts/i18n/translations.json +++ b/app/assets/javascripts/i18n/translations.json @@ -188,11 +188,7 @@ "date_on": "on", "description_languages": "Language of the description", "download_button": { - "download": "Download", - "downloading": "Preparing submissions for download, this might take a couple of minutes. Do not close this page.", - "go_back": "Go back", - "ready": "Export is ready. The download should start automatically.", - "retry": "Retry" + "ready": "Export is ready. The download should start automatically." }, "dropdown": { "multi": { @@ -719,13 +715,6 @@ "date_before": "voor", "date_on": "op", "description_languages": "Taal van de beschrijving", - "download_button": { - "download": "Download", - "downloading": "Bezig met het voorbereiden van de download, dit kan enkele minuten duren. Sluit deze pagina niet.", - "go_back": "Keer terug", - "ready": "De export is klaar. De download start automatisch.", - "retry": "Probeer opnieuw" - }, "dropdown": { "multi": { "activity_types": "Activiteitstypes", @@ -763,6 +752,9 @@ }, "en": "Engels", "event_types": "Event types", + "export": { + "ready": "De export is klaar. De download start normaal automatisch." + }, "favorite-course-do": "Voeg toe aan favorieten", "favorite-course-failed": "Cursus aan favorieten toevoegen mislukt", "favorite-course-succeeded": "Cursus toegevoegd aan favorieten", diff --git a/app/javascript/packs/export.js b/app/javascript/packs/export.js index 00fdd0708b..0cda9bfeaf 100644 --- a/app/javascript/packs/export.js +++ b/app/javascript/packs/export.js @@ -1,4 +1,4 @@ import { initSelection } from "export.ts"; -import "components/download_button"; +import "components/search/loading_bar.ts"; window.dodona.initSelection = initSelection; diff --git a/app/views/exports/download_submissions.html.erb b/app/views/exports/download_submissions.html.erb index 4d0b83f362..09923028e2 100644 --- a/app/views/exports/download_submissions.html.erb +++ b/app/views/exports/download_submissions.html.erb @@ -146,7 +146,34 @@ <% end %>
- + +
+ + + + + From 0e4f069f123ac7402472df649fff76ae4858f231 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Mon, 11 Mar 2024 11:54:44 +0100 Subject: [PATCH 15/18] Fix disabled styling --- app/assets/javascripts/export.ts | 2 +- app/assets/stylesheets/components/btn.css.scss | 3 ++- app/views/exports/download_submissions.html.erb | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/export.ts b/app/assets/javascripts/export.ts index 23ca4ffb03..977d1cadba 100644 --- a/app/assets/javascripts/export.ts +++ b/app/assets/javascripts/export.ts @@ -113,7 +113,7 @@ function initSelection(): void { // Update the retry download button const retryButton = document.getElementById("retry-download-button") as HTMLAnchorElement; retryButton.href = downloadUrl; - retryButton.attributes.removeNamedItem("disabled"); + retryButton.classList.remove("disabled"); window.location.href = downloadUrl; }); diff --git a/app/assets/stylesheets/components/btn.css.scss b/app/assets/stylesheets/components/btn.css.scss index 1e3684c8a0..ecb4728d61 100644 --- a/app/assets/stylesheets/components/btn.css.scss +++ b/app/assets/stylesheets/components/btn.css.scss @@ -134,7 +134,8 @@ border-color: var(--d-btn-color); } - &:disabled { + &.disabled, + &:disabled{ color: var(--d-on-surface); border: 1px solid rgba(var(--d-on-surface-rgb), 0.12); opacity: 0.38; diff --git a/app/views/exports/download_submissions.html.erb b/app/views/exports/download_submissions.html.erb index 56eed41281..0ea3393059 100644 --- a/app/views/exports/download_submissions.html.erb +++ b/app/views/exports/download_submissions.html.erb @@ -168,7 +168,7 @@
- + <%= t '.retry_download_button' %>
diff --git a/config/locales/views/exports/en.yml b/config/locales/views/exports/en.yml index db6991da3a..d18fa86e6a 100644 --- a/config/locales/views/exports/en.yml +++ b/config/locales/views/exports/en.yml @@ -49,4 +49,3 @@ en: download: Download file downloading: Preparing submissions for download, this might take a couple of minutes. Do not close this page. retry_download_button: Retry download - back_button: Go back diff --git a/config/locales/views/exports/nl.yml b/config/locales/views/exports/nl.yml index 55b9a1e744..44540ce216 100644 --- a/config/locales/views/exports/nl.yml +++ b/config/locales/views/exports/nl.yml @@ -49,4 +49,3 @@ nl: download: Bestand downloaden downloading: Bezig met het voorbereiden van de download, dit kan enkele minuten duren. Sluit deze pagina niet. retry_download_button: Probeer opnieuw - back_button: Keer terug From c5cc60be58d8c4a7f50c67e923da338b0d9109da Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Mon, 11 Mar 2024 13:55:40 +0100 Subject: [PATCH 17/18] Remove buttons --- app/assets/javascripts/export.ts | 8 +------- app/assets/javascripts/i18n/translations.json | 8 ++++---- app/views/exports/download_submissions.html.erb | 5 ----- config/locales/js/en.yml | 4 ++-- config/locales/js/nl.yml | 2 +- config/locales/views/exports/en.yml | 1 - config/locales/views/exports/nl.yml | 1 - 7 files changed, 8 insertions(+), 21 deletions(-) diff --git a/app/assets/javascripts/export.ts b/app/assets/javascripts/export.ts index 977d1cadba..bd7e8a69d8 100644 --- a/app/assets/javascripts/export.ts +++ b/app/assets/javascripts/export.ts @@ -107,13 +107,7 @@ function initSelection(): void { const downloadUrl = await exportLocation(exportDataUrl); // Update the stepper content - downloadingPanel.querySelector(".stepper-part").replaceChildren( - document.createTextNode(i18n.t("js.export.ready")) - ); - // Update the retry download button - const retryButton = document.getElementById("retry-download-button") as HTMLAnchorElement; - retryButton.href = downloadUrl; - retryButton.classList.remove("disabled"); + downloadingPanel.querySelector(".stepper-part").innerHTML = i18n.t("js.export.ready_html", { url: downloadUrl }); window.location.href = downloadUrl; }); diff --git a/app/assets/javascripts/i18n/translations.json b/app/assets/javascripts/i18n/translations.json index 4f24e03ede..4ab3c6a58f 100644 --- a/app/assets/javascripts/i18n/translations.json +++ b/app/assets/javascripts/i18n/translations.json @@ -187,9 +187,6 @@ "date_before": "before", "date_on": "on", "description_languages": "Language of the description", - "download_button": { - "ready": "Export is ready. The download should start automatically." - }, "dropdown": { "multi": { "activity_types": "Activity types", @@ -227,6 +224,9 @@ }, "en": "English", "event_types": "Event types", + "export": { + "ready_html": "Export is ready. The download should start automatically. If not, click this link to start the download." + }, "favorite-course-do": "Favorite", "favorite-course-failed": "Favoriting course failed", "favorite-course-succeeded": "Favorited course", @@ -753,7 +753,7 @@ "en": "Engels", "event_types": "Event types", "export": { - "ready": "De export is klaar. De download start normaal automatisch." + "ready_html": "De export is klaar. De download start normaal automatisch. Als dit niet gebeurt, klik dan hier." }, "favorite-course-do": "Voeg toe aan favorieten", "favorite-course-failed": "Cursus aan favorieten toevoegen mislukt", diff --git a/app/views/exports/download_submissions.html.erb b/app/views/exports/download_submissions.html.erb index a84a90d644..f50b7d54e6 100644 --- a/app/views/exports/download_submissions.html.erb +++ b/app/views/exports/download_submissions.html.erb @@ -167,11 +167,6 @@

<%= t '.downloading' %>

- diff --git a/config/locales/js/en.yml b/config/locales/js/en.yml index 20a21f912a..6bfc4dedcd 100644 --- a/config/locales/js/en.yml +++ b/config/locales/js/en.yml @@ -327,6 +327,6 @@ en: feedbacks: submission: submit: Submit this solution - download_button: - ready: Export is ready. The download should start automatically. + export: + ready_html: Export is ready. The download should start automatically. If not, click this link to start the download. diff --git a/config/locales/js/nl.yml b/config/locales/js/nl.yml index e9102f57f4..f5729f1b77 100644 --- a/config/locales/js/nl.yml +++ b/config/locales/js/nl.yml @@ -328,4 +328,4 @@ nl: submission: submit: Deze oplossing indienen export: - ready: De export is klaar. De download start normaal automatisch. + ready_html: De export is klaar. De download start normaal automatisch. Als dit niet gebeurt, klik dan hier. diff --git a/config/locales/views/exports/en.yml b/config/locales/views/exports/en.yml index d18fa86e6a..319f3e0611 100644 --- a/config/locales/views/exports/en.yml +++ b/config/locales/views/exports/en.yml @@ -48,4 +48,3 @@ en: no_series: no-series download: Download file downloading: Preparing submissions for download, this might take a couple of minutes. Do not close this page. - retry_download_button: Retry download diff --git a/config/locales/views/exports/nl.yml b/config/locales/views/exports/nl.yml index 44540ce216..859aa853d6 100644 --- a/config/locales/views/exports/nl.yml +++ b/config/locales/views/exports/nl.yml @@ -48,4 +48,3 @@ nl: no_series: geen-reeks download: Bestand downloaden downloading: Bezig met het voorbereiden van de download, dit kan enkele minuten duren. Sluit deze pagina niet. - retry_download_button: Probeer opnieuw From c4abf8e16fb6d52a325eaf2c902e28643a711e5c Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Thu, 14 Mar 2024 16:07:27 +0100 Subject: [PATCH 18/18] Update app/assets/stylesheets/components/btn.css.scss Co-authored-by: Charlotte Van Petegem --- app/assets/stylesheets/components/btn.css.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/components/btn.css.scss b/app/assets/stylesheets/components/btn.css.scss index ecb4728d61..0637169121 100644 --- a/app/assets/stylesheets/components/btn.css.scss +++ b/app/assets/stylesheets/components/btn.css.scss @@ -135,7 +135,7 @@ } &.disabled, - &:disabled{ + &:disabled { color: var(--d-on-surface); border: 1px solid rgba(var(--d-on-surface-rgb), 0.12); opacity: 0.38;