From 45f6b4daf2ae056a7312951ad480a8f3e7f26355 Mon Sep 17 00:00:00 2001
From: jorg-vr
Date: Mon, 4 Mar 2024 11:21:07 +0100
Subject: [PATCH 1/7] Allow exporting index as csv
---
app/controllers/course_members_controller.rb | 13 ++++++++++++-
app/views/course_members/index.csv.erb | 4 ++++
2 files changed, 16 insertions(+), 1 deletion(-)
create mode 100644 app/views/course_members/index.csv.erb
diff --git a/app/controllers/course_members_controller.rb b/app/controllers/course_members_controller.rb
index 86fd70b5a2..94685dbfb9 100644
--- a/app/controllers/course_members_controller.rb
+++ b/app/controllers/course_members_controller.rb
@@ -33,11 +33,22 @@ def index
@course_memberships = apply_scopes(@course.course_memberships.order_by_status_in_course_and_name('ASC'))
.includes(:course_labels, user: [:institution])
.where(status: statuses)
- .paginate(page: parse_pagination_param(params[:page]))
@title = I18n.t('courses.index.users')
@crumbs = [[@course.name, course_path(@course)], [I18n.t('courses.index.users'), '#']]
@course_labels = CourseLabel.where(course: @course)
+
+ respond_to do |format|
+ format.html do
+ @course_memberships = @course_memberships.paginate(page: parse_pagination_param(params[:page]))
+ end
+ format.js do
+ @course_memberships = @course_memberships.paginate(page: parse_pagination_param(params[:page]))
+ end
+ format.csv do
+ headers['Content-Disposition'] = "attachment; filename=\"#{@course.name} - #{I18n.t('courses.index.users')}.csv\""
+ end
+ end
end
def show
diff --git a/app/views/course_members/index.csv.erb b/app/views/course_members/index.csv.erb
new file mode 100644
index 0000000000..9211b68d79
--- /dev/null
+++ b/app/views/course_members/index.csv.erb
@@ -0,0 +1,4 @@
+<%= CSV.generate_line %w[id username last_name first_name email labels], row_sep: nil %>
+<% @course_memberships.each do |cm| %>
+ <%= CSV.generate_line([cm.user.id, cm.user.username, cm.user.last_name, cm.user.first_name, cm.user.email, cm.course_labels.map(&:name).join(';')], row_sep: nil).html_safe %>
+<% end %>
From 5364904f3a4f563ef0f431d133b2ba4ea9f976f5 Mon Sep 17 00:00:00 2001
From: jorg-vr
Date: Mon, 4 Mar 2024 11:24:10 +0100
Subject: [PATCH 2/7] Remove old labels csv method
---
app/controllers/course_members_controller.rb | 9 ---------
app/models/course.rb | 16 ----------------
app/views/course_members/index.html.erb | 2 +-
config/routes.rb | 1 -
4 files changed, 1 insertion(+), 27 deletions(-)
diff --git a/app/controllers/course_members_controller.rb b/app/controllers/course_members_controller.rb
index 94685dbfb9..838eb9b6af 100644
--- a/app/controllers/course_members_controller.rb
+++ b/app/controllers/course_members_controller.rb
@@ -81,15 +81,6 @@ def update
end
end
- def download_labels_csv
- csv = @course.labels_csv
- send_data csv[:data],
- type: 'application/csv',
- filename: csv[:filename],
- disposition: 'attachment',
- x_sendfile: true
- end
-
def upload_labels_csv
return render json: { message: I18n.t('course_members.upload_labels_csv.no_file') }, status: :unprocessable_entity if params[:file] == 'undefined'
diff --git a/app/models/course.rb b/app/models/course.rb
index 708e4ca7cc..2b31b99e03 100644
--- a/app/models/course.rb
+++ b/app/models/course.rb
@@ -393,22 +393,6 @@ def scoresheet
}
end
- def labels_csv
- sorted_course_memberships = course_memberships
- .where.not(status: %i[unsubscribed pending])
- .includes(:user)
- .order(status: :asc)
- .order(Arel.sql('users.permission ASC'))
- .order(Arel.sql('users.last_name ASC'), Arel.sql('users.first_name ASC'))
- data = CSV.generate(force_quotes: true) do |csv|
- csv << %w[id username last_name first_name email labels]
- sorted_course_memberships.each do |cm|
- csv << [cm.user.id, cm.user.username, cm.user.last_name, cm.user.first_name, cm.user.email, cm.course_labels.map(&:name).join(';')]
- end
- end
- { filename: "#{name}-users-labels.csv", data: data }
- end
-
def self.format_year(year)
year.sub(/ ?- ?/, '–')
end
diff --git a/app/views/course_members/index.html.erb b/app/views/course_members/index.html.erb
index a082506f63..a502417e7f 100644
--- a/app/views/course_members/index.html.erb
+++ b/app/views/course_members/index.html.erb
@@ -35,7 +35,7 @@
<%= t ".first_download_labels" %>
- <%= t ".download" %>
+ <%= t ".download" %>
diff --git a/config/routes.rb b/config/routes.rb
index a151851658..57b84ef61a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -98,7 +98,6 @@
resources :submissions, only: [:index]
resources :activity_read_states, only: [:index]
resources :members, only: %i[index show edit update], controller: :course_members do
- get 'download_labels_csv', on: :collection
post 'upload_labels_csv', on: :collection
end
member do
From 3caebf39604787a44935f6a0c5e075d5e118b463 Mon Sep 17 00:00:00 2001
From: jorg-vr
Date: Mon, 4 Mar 2024 11:37:54 +0100
Subject: [PATCH 3/7] rmove unused click option
---
app/views/annotations/question_index.html.erb | 2 +-
app/views/submissions/index.html.erb | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/views/annotations/question_index.html.erb b/app/views/annotations/question_index.html.erb
index cd2b08c4aa..ca6c5702c4 100644
--- a/app/views/annotations/question_index.html.erb
+++ b/app/views/annotations/question_index.html.erb
@@ -23,7 +23,7 @@
<% actions = [] %>
<% if current_user&.a_course_admin? %>
- <% actions << { text: t('questions.index.watch'), search: { refresh: true }, click: 'window.dodona.toggleIndexReload()' } %>
+ <% actions << { text: t('questions.index.watch'), search: { refresh: true } } %>
<% actions << { text: t('questions.index.everything'), search: { everything: true } } if @unfiltered %>
<% end %>
<%= render partial: 'layouts/searchbar', locals: { actions: actions, refresh_element: "#question-container", courses: @courses, question_states: Question.question_states.keys } %>
diff --git a/app/views/submissions/index.html.erb b/app/views/submissions/index.html.erb
index fdc9b655e4..854c7627df 100644
--- a/app/views/submissions/index.html.erb
+++ b/app/views/submissions/index.html.erb
@@ -39,7 +39,7 @@
<%
actions << {icon: 'replay', text: t(".reevaluate_submissions"), confirm: t(".confirm_reevaluate_submissions"), action: mass_rejudge_submissions_path(user_id: @user&.id, activity_id: @activity&.id, course_id: @course&.id, series_id: @series&.id, judge_id: @judge&.id)} if policy(Submission).mass_rejudge?
actions << {icon: 'done', text: t('.most_recent'), search: {most_recent_per_user: true}} if @activity
- actions << {icon: 'file-eye', text: t('.watch_submissions'), search: {refresh: true}, click: 'window.dodona.toggleIndexReload()'}
+ actions << {icon: 'file-eye', text: t('.watch_submissions'), search: {refresh: true}}
%>
<% end %>
<%= render partial: 'layouts/searchbar', locals: {actions: actions, course_labels: @course_labels, statuses: Submission.statuses.keys, refresh_element: "#refresh_element"} %>
From 84be4a0414345b65c6c566267468e9999847bab6 Mon Sep 17 00:00:00 2001
From: jorg-vr
Date: Mon, 4 Mar 2024 11:52:07 +0100
Subject: [PATCH 4/7] Make downloading filtered user list discoverable
---
.../javascripts/components/search/search_actions.ts | 9 +++++++--
app/views/course_members/index.html.erb | 5 +++++
config/locales/views/course_members/en.yml | 1 +
config/locales/views/course_members/nl.yml | 1 +
4 files changed, 14 insertions(+), 2 deletions(-)
diff --git a/app/assets/javascripts/components/search/search_actions.ts b/app/assets/javascripts/components/search/search_actions.ts
index bbaee86266..f015cfaae4 100644
--- a/app/assets/javascripts/components/search/search_actions.ts
+++ b/app/assets/javascripts/components/search/search_actions.ts
@@ -95,10 +95,16 @@ export class SearchActions extends DodonaElement {
}
async performAction(action: SearchAction): Promise {
- if (!action.action && !action.js) {
+ if (!action.action && !action.js && !action.url) {
return true;
}
+ if (action.url) {
+ const url: string = searchQueryState.addParametersToUrl(action.url);
+ window.open(url);
+ return false;
+ }
+
if (!action.action) {
eval(action.js);
return false;
@@ -147,7 +153,6 @@ export class SearchActions extends DodonaElement {
${this.getSearchActions().map(action => html`
this.performAction(action)}
>
diff --git a/app/views/course_members/index.html.erb b/app/views/course_members/index.html.erb
index a502417e7f..9037d2ffbc 100644
--- a/app/views/course_members/index.html.erb
+++ b/app/views/course_members/index.html.erb
@@ -82,6 +82,11 @@
text: t(".edit_all_labels"),
js: 'bootstrap.Modal.getOrCreateInstance(document.querySelector("#labelsUploadModal")).show()',
type: 'enrolled'
+ },
+ {
+ icon: 'cloud-download',
+ text: t(".download_user_csv"),
+ url: course_members_path(@course, format: :csv),
}
],
course_labels: @course_labels,
diff --git a/config/locales/views/course_members/en.yml b/config/locales/views/course_members/en.yml
index 8c5356c03b..008b92c0cb 100644
--- a/config/locales/views/course_members/en.yml
+++ b/config/locales/views/course_members/en.yml
@@ -25,6 +25,7 @@ en:
close: "Close"
upload: "Upload changes"
edit_all_labels: "Edit all labels"
+ download_user_csv: "Download user list"
first_download_labels: "Download a list of all users and their labels as a CSV file."
then_edit: "Open the file you just downloaded and edit the labels. Only changes to the labels column will be used. Note that the labels are in one field, separated by semicolons."
finally_upload: "When you are happy with your changes, select the modified file below."
diff --git a/config/locales/views/course_members/nl.yml b/config/locales/views/course_members/nl.yml
index 0da12c53f4..b17d1ce582 100644
--- a/config/locales/views/course_members/nl.yml
+++ b/config/locales/views/course_members/nl.yml
@@ -25,6 +25,7 @@ nl:
close: "Sluiten"
upload: "Aanpassingen uploaden"
edit_all_labels: "Alle labels bewerken"
+ download_user_csv: "Gebruikerslijst downloaden"
first_download_labels: "Download een lijst van alle gebruikers en hun labels als een CSV bestand."
then_edit: "Open het tekstbestand dat je net gedownload hebt, en pas de labels aan. Enkel wijzigingen aan de labels kolom zullen doorgevoerd worden. Merk op dat de labels in één veld zitten, gescheiden met puntkomma's."
finally_upload: "Als je tevreden bent met je aanpassingen, selecteer hieronder dan het aangepaste bestand."
From 361204eb42777de2314d88210640d1c4af631e5d Mon Sep 17 00:00:00 2001
From: jorg-vr
Date: Mon, 4 Mar 2024 11:57:17 +0100
Subject: [PATCH 5/7] Add progress
---
app/views/course_members/index.csv.erb | 13 +++++++++++--
1 file changed, 11 insertions(+), 2 deletions(-)
diff --git a/app/views/course_members/index.csv.erb b/app/views/course_members/index.csv.erb
index 9211b68d79..202d5e676d 100644
--- a/app/views/course_members/index.csv.erb
+++ b/app/views/course_members/index.csv.erb
@@ -1,4 +1,13 @@
-<%= CSV.generate_line %w[id username last_name first_name email labels], row_sep: nil %>
+<%= CSV.generate_line %w[id username last_name first_name email correct_exercises attempted_exercises labels], row_sep: nil %>
<% @course_memberships.each do |cm| %>
- <%= CSV.generate_line([cm.user.id, cm.user.username, cm.user.last_name, cm.user.first_name, cm.user.email, cm.course_labels.map(&:name).join(';')], row_sep: nil).html_safe %>
+ <%= CSV.generate_line([
+ cm.user.id,
+ cm.user.username,
+ cm.user.last_name,
+ cm.user.first_name,
+ cm.user.email,
+ cm.user.correct_exercises(course: @course),
+ cm.user.attempted_exercises(course: @course),
+ cm.course_labels.map(&:name).join(';')
+ ], row_sep: nil).html_safe %>
<% end %>
From 0e5d4196e490cdcb3d6b6ebaf7d368ea5b342ed2 Mon Sep 17 00:00:00 2001
From: jorg-vr
Date: Mon, 4 Mar 2024 12:01:29 +0100
Subject: [PATCH 6/7] Avoid tab
---
app/views/course_members/index.csv.erb | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/app/views/course_members/index.csv.erb b/app/views/course_members/index.csv.erb
index 202d5e676d..fb9f0e77f5 100644
--- a/app/views/course_members/index.csv.erb
+++ b/app/views/course_members/index.csv.erb
@@ -1,13 +1,13 @@
<%= CSV.generate_line %w[id username last_name first_name email correct_exercises attempted_exercises labels], row_sep: nil %>
<% @course_memberships.each do |cm| %>
- <%= CSV.generate_line([
- cm.user.id,
- cm.user.username,
- cm.user.last_name,
- cm.user.first_name,
- cm.user.email,
- cm.user.correct_exercises(course: @course),
- cm.user.attempted_exercises(course: @course),
- cm.course_labels.map(&:name).join(';')
- ], row_sep: nil).html_safe %>
+<%= CSV.generate_line([
+ cm.user.id,
+ cm.user.username,
+ cm.user.last_name,
+ cm.user.first_name,
+ cm.user.email,
+ cm.user.correct_exercises(course: @course),
+ cm.user.attempted_exercises(course: @course),
+ cm.course_labels.map(&:name).join(';')
+], row_sep: nil).html_safe %>
<% end %>
From 0691f53799f18ec174c56fbfd8b57054ede30f1d Mon Sep 17 00:00:00 2001
From: jorg-vr
Date: Mon, 4 Mar 2024 14:18:41 +0100
Subject: [PATCH 7/7] Fix tests
---
.../components/search/search_actions.test.ts | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/test/javascript/components/search/search_actions.test.ts b/test/javascript/components/search/search_actions.test.ts
index a357cde1a2..1f87d55fc9 100644
--- a/test/javascript/components/search/search_actions.test.ts
+++ b/test/javascript/components/search/search_actions.test.ts
@@ -58,7 +58,16 @@ describe("SearchActions", () => {
});
test("clicking a link action should navigate to the url", async () => {
- expect(screen.getByText("link-test").closest("a").href).toBe("https://test.dodona.be/");
+ jest.spyOn(window, "open").mockImplementation(() => window);
+ await userEvent.click(screen.queryByText("link-test"));
+ expect(window.open).toHaveBeenCalledWith("https://test.dodona.be");
+ });
+
+ test("clicking a link action should add query params to the url", async () => {
+ jest.spyOn(window, "open").mockImplementation(() => window);
+ searchQueryState.queryParams.set("foo", "bar");
+ await userEvent.click(screen.queryByText("link-test"));
+ expect(window.open).toHaveBeenCalledWith("https://test.dodona.be/?foo=bar");
});
test("clicking a confirm action should show a confirmation dialog", async () => {