Skip to content
This repository was archived by the owner on Jun 24, 2024. It is now read-only.

Commit 060eb90

Browse files
committed
Merge branch 'roster-add-active-job' of https://github.com/education/classroom into roster-add-active-job
2 parents 9f988b7 + 9f81fbb commit 060eb90

File tree

12 files changed

+373
-200
lines changed

12 files changed

+373
-200
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
(function(){
2+
var PROGRESS_HALF_LIFE = 1000;
3+
var setup_roster_update_cable,
4+
progress_asymptotically,
5+
display_message,
6+
set_progress,
7+
display_progress_bar,
8+
initialize_progress;
9+
10+
var progress_complete = false;
11+
setup_roster_update_cable = function(){
12+
var roster_id = $("#current_roster_id").val();
13+
var user_id = $("#current_user_id").val();
14+
App.add_students_to_roster = App.cable.subscriptions.create({
15+
channel: "AddStudentsToRosterChannel",
16+
roster_id: roster_id,
17+
user_id: user_id
18+
},
19+
{
20+
connected: function() {
21+
// Called when the subscription is ready for use on the server
22+
},
23+
24+
disconnected: function() {
25+
// Called when the subscription has been terminated by the server
26+
},
27+
28+
received: function(data) {
29+
// Called when there's incoming data on the websocket for this channel
30+
if(data.status == "completed"){
31+
progress_complete = true;
32+
set_progress(100);
33+
display_message(data.message);
34+
}
35+
}
36+
});
37+
};
38+
39+
progress_asymptotically = function() {
40+
recursive_progress_asymptotically = function(recursive_callback, counter) {
41+
var progress;
42+
var remaining = 100/counter;
43+
progress = 100 - (100/counter);
44+
$(document)
45+
.find(".roster-update-progress-bar")
46+
.animate(
47+
{ width: progress.toFixed() + "%" },
48+
{ duration: PROGRESS_HALF_LIFE * counter }
49+
);
50+
setTimeout(function() {
51+
if(!progress_complete){
52+
recursive_callback(recursive_callback, counter + 1);
53+
}
54+
},
55+
PROGRESS_HALF_LIFE * counter
56+
);
57+
};
58+
recursive_progress_asymptotically(recursive_progress_asymptotically, 1);
59+
};
60+
61+
display_message = function(message){
62+
$(".roster-update-message").removeAttr("hidden");
63+
$("#roster-progress").text(message);
64+
};
65+
66+
set_progress = function(percent) {
67+
$(".roster-update-progress-bar").stop(true, false);
68+
if (percent === 0) {
69+
$(".roster-update-progress-bar").css("width", 0);
70+
} else if (percent) {
71+
$(".roster-update-progress-bar").animate({width: percent + "%"});
72+
}
73+
};
74+
75+
toggle_roster_form = function(disable_fields){
76+
var text_area = $("#entries-field");
77+
var csv_button = $("#file-upload");
78+
if(disable_fields){
79+
text_area.attr("disabled", "disabled");
80+
csv_button.addClass("disabled");
81+
}else{
82+
text_area.removeAttr("disabled");
83+
csv_button.removeClass("disabled");
84+
}
85+
};
86+
87+
ready = (function(){
88+
$("#add-students-roster-form").on("ajax:beforeSend", function(){
89+
toggle_roster_form(true);
90+
$('.roster-update-progress').removeAttr("hidden");
91+
progress_asymptotically();
92+
});
93+
94+
$("#add-students-roster-form").on("ajax:complete", function(){
95+
toggle_roster_form(false);
96+
$("#entries-field").val("");
97+
});
98+
99+
$(document).on('closing', '[data-remodal-id=new-student-modal]', function (e) {
100+
set_progress("0");
101+
$('.roster-update-progress').attr("hidden", "hidden");
102+
});
103+
104+
setup_roster_update_cable();
105+
});
106+
107+
$(document).ready(ready);
108+
109+
}).call(this);
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# frozen_string_literal: true
2+
3+
class AddStudentsToRosterChannel < ApplicationCable::Channel
4+
def self.channel(user_id:, roster_id:)
5+
"#{channel_name}_#{user_id}_#{roster_id}"
6+
end
7+
8+
def subscribed
9+
stream_from self.class.channel(roster_id: params[:roster_id], user_id: current_user.id)
10+
end
11+
12+
def unsubscribed
13+
# Any cleanup needed when channel is unsubscribed
14+
end
15+
end

app/controllers/orgs/rosters_controller.rb

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class RostersController < Orgs::Controller
2121
depends_on :google_classroom
2222

2323
# rubocop:disable AbcSize
24+
# rubocop:disable MethodLength
2425
def show
2526
@google_course_name = current_organization_google_course_name
2627

@@ -34,9 +35,13 @@ def show
3435
.order(:id)
3536
.page(params[:unlinked_users_page])
3637

38+
# Used for displaying all roster entries in tabs
39+
@roster_entries_count = current_roster.roster_entries.count
40+
3741
download_roster if params.dig("format")
3842
end
3943
# rubocop:enable AbcSize
44+
# rubocop:enable MethodLength
4045

4146
def new
4247
@roster = Roster.new
@@ -129,38 +134,19 @@ def edit_entry
129134
redirect_to roster_path(current_organization, params: { roster_entries_page: params[:roster_entries_page] })
130135
end
131136

132-
# rubocop:disable Metrics/MethodLength
133137
# rubocop:disable Metrics/AbcSize
134138
def add_students
135-
if params[:lms_user_ids].is_a? String
136-
params[:lms_user_ids] = params[:lms_user_ids].split
137-
end
138-
identifiers = params[:identifiers].split("\r\n").reject(&:blank?)
139-
lms_ids = params[:lms_user_ids] || []
140-
141-
begin
142-
entries = RosterEntry.create_entries(
143-
identifiers: identifiers,
144-
roster: current_roster,
145-
lms_user_ids: lms_ids
146-
)
147-
148-
if entries.empty?
149-
flash[:warning] = "No students created."
150-
elsif entries.length == identifiers.length
151-
flash[:success] = "Students created."
152-
imported_students_lms_statsd(lms_user_ids: params[:lms_user_ids])
153-
else
154-
flash[:success] = "Students created. Some duplicates have been omitted."
155-
imported_students_lms_statsd(lms_user_ids: params[:lms_user_ids])
156-
end
157-
rescue RosterEntry::IdentifierCreationError
158-
flash[:error] = "An error has occurred. Please try again."
159-
end
139+
params[:lms_user_ids].split! if params[:lms_user_ids].is_a? String
140+
lms_user_ids = Array.wrap(params[:lms_user_ids])
160141

161-
redirect_to roster_path(current_organization)
142+
identifiers = params[:identifiers].split("\r\n").reject(&:blank?)
143+
job_info = AddStudentsToRosterJob.perform_later(identifiers, current_roster, current_user, lms_user_ids)
144+
render json: { job_info: job_info }
162145
end
146+
# rubocop:enable Metrics/AbcSize
163147

148+
# rubocop:disable Metrics/MethodLength
149+
# rubocop:disable Metrics/AbcSize
164150
def download_roster
165151
grouping = current_organization.groupings.find(params[:grouping]) if params[:grouping]
166152

@@ -177,7 +163,6 @@ def download_roster
177163
end
178164
end
179165
end
180-
181166
# rubocop:enable Metrics/MethodLength
182167
# rubocop:enable Metrics/AbcSize
183168

@@ -194,7 +179,7 @@ def create_statsd(lms_user_ids:)
194179
end
195180

196181
def imported_students_lms_statsd(lms_user_ids:)
197-
return if lms_user_ids.nil?
182+
return if lms_user_ids.blank?
198183
GitHubClassroom.statsd.increment("roster_entries.lms_imported", by: lms_user_ids.length)
199184
end
200185

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# frozen_string_literal: true
2+
3+
class AddStudentsToRosterJob < ApplicationJob
4+
queue_as :roster
5+
6+
ROSTER_UPDATE_SUCCESSFUL = "Roster successfully updated."
7+
ROSTER_UPDATE_FAILED = "Could not add any students to roster, please try again."
8+
ROSTER_UPDATE_PARTIAL_SUCCESS = "Could not add following students:"
9+
10+
# Takes an array of identifiers and creates a
11+
# roster entry for each. Omits duplicates, and
12+
#
13+
# rubocop:disable AbcSize
14+
# rubocop:disable MethodLength
15+
def perform(identifiers, roster, user, lms_user_ids = [])
16+
channel = AddStudentsToRosterChannel.channel(roster_id: roster.id, user_id: user.id)
17+
18+
identifiers = add_suffix_to_duplicates!(identifiers, roster)
19+
invalid_roster_entries =
20+
identifiers.zip(lms_user_ids).map do |identifier, lms_user_id|
21+
roster_entry = RosterEntry.create(identifier: identifier, roster: roster, lms_user_id: lms_user_id)
22+
roster_entry.identifier if roster_entry.errors.include?(:identifier)
23+
end.compact!
24+
25+
message = build_message(invalid_roster_entries, identifiers)
26+
entries_created = identifiers.count - invalid_roster_entries.count
27+
if lms_user_ids.present? && entries_created.positive?
28+
GitHubClassroom.statsd.increment("roster_entries.lms_imported", by: entries_created)
29+
end
30+
ActionCable.server.broadcast(channel, message: message, status: "completed")
31+
end
32+
# rubocop:enable AbcSize
33+
# rubocop:enable MethodLength
34+
35+
def add_suffix_to_duplicates!(identifiers, roster)
36+
existing_roster_entries = RosterEntry.where(roster: roster).pluck(:identifier)
37+
RosterEntry.add_suffix_to_duplicates(
38+
identifiers: identifiers,
39+
existing_roster_entries: existing_roster_entries
40+
)
41+
end
42+
43+
# rubocop:disable MethodLength
44+
def build_message(invalid_roster_entries, identifiers)
45+
if invalid_roster_entries.empty?
46+
ROSTER_UPDATE_SUCCESSFUL + " #{identifiers.count} roster #{'entries'.pluralize(identifiers.count)} were added."
47+
elsif invalid_roster_entries.size == identifiers.size
48+
ROSTER_UPDATE_FAILED
49+
else
50+
formatted_students =
51+
invalid_roster_entries.map do |invalid_roster_entry|
52+
"#{invalid_roster_entry} \n"
53+
end.join("")
54+
"#{ROSTER_UPDATE_PARTIAL_SUCCESS} \n#{formatted_students}"
55+
end
56+
end
57+
# rubocop:enable MethodLength
58+
end

app/models/roster_entry.rb

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -112,35 +112,4 @@ def self.students_not_on_team(group_assignment)
112112
.uniq
113113
where(user_id: nil).or(where.not(user_id: students_on_team))
114114
end
115-
116-
# Takes an array of identifiers and creates a
117-
# roster entry for each. Omits duplicates, and
118-
# raises IdentifierCreationError if there is an
119-
# error.
120-
#
121-
# Returns the created entries.
122-
123-
# rubocop:disable Metrics/MethodLength
124-
def self.create_entries(identifiers:, roster:, lms_user_ids: [])
125-
created_entries = []
126-
RosterEntry.transaction do
127-
identifiers = add_suffix_to_duplicates(
128-
identifiers: identifiers,
129-
existing_roster_entries: RosterEntry.where(roster: roster).pluck(:identifier)
130-
)
131-
132-
identifiers.zip(lms_user_ids).each do |identifier, lms_user_id|
133-
roster_entry = RosterEntry.create(identifier: identifier, roster: roster, lms_user_id: lms_user_id)
134-
135-
if !roster_entry.persisted?
136-
raise IdentifierCreationError unless roster_entry.errors.include?(:identifier)
137-
else
138-
created_entries << roster_entry
139-
end
140-
end
141-
end
142-
143-
created_entries
144-
end
145-
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
146115
end

app/views/orgs/rosters/_new_student_modal.html.erb

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,27 @@
44
<button data-remodal-action="close" class="remodal-close"><%= octicon 'x' %></button>
55
<h2 class="remodal-header text-left">Add students to your Roster</h2>
66

7+
<%= hidden_field_tag 'user_id', current_user.id, id: "current_user_id" %>
8+
<%= hidden_field_tag 'roster_id', roster.id, id: "current_roster_id" %>
9+
10+
<div class="flex-justify-between roster-update-progress" hidden>
11+
<div class="Box Box--blue">
12+
<div class="flash flash-full roster-update-message" hidden>
13+
<button class="flash-close js-flash-close"><%= octicon "x" %></button>
14+
<p id="roster-progress"></p>
15+
</div>
16+
<div class="Box-body">
17+
<div class="Subhead border-bottom-0">
18+
<h4 class="Subhead-heading Subhead-heading--blue">Adding students to Roster</h4>
19+
</div>
20+
<span class="Progress Progress">
21+
<span class="bg-green roster-update-progress-bar" style="width: 0%;"></span>
22+
</span>
23+
</div>
24+
</div>
25+
</div>
26+
27+
728
<div class="my-3">
829
<h2 class="h4 mb-2">Import students from your institution</h2>
930
<p>GitHub Classroom is able to automatically import your roster from your institution. If you
@@ -18,7 +39,7 @@
1839
<div class="my-3">
1940
<h2 class="h4 mb-2">Manually add students</h2>
2041
<div>
21-
<%= form_tag add_students_roster_path(current_organization), method: :patch do |f| %>
42+
<%= form_tag add_students_roster_path(current_organization), method: :patch, remote: true, id: "add-students-roster-form" do |f| %>
2243
<div class="Box">
2344
<dl class="Box-body <%= view.form_class_for(:roster_entries) %> mb-0">
2445
<dt class="text-gray mb-1">Enter your list of students' identifiers, <strong>one per line</strong>.</dt>
@@ -32,8 +53,8 @@
3253
</div>
3354
</div>
3455

35-
<div class="d-flex flex-items-center border-top pt-5">
36-
<%= submit_tag 'Add roster entries', class: 'btn btn-primary mr-3' %>
56+
<div class="d-flex flex-items-center border-top pt-5">
57+
<%= submit_tag 'Add roster entries', class: 'btn btn-primary mr-3', data: { "disable-with": "Updating..." } %>
3758
</div>
3859
<% end %>
3960
</div>

app/views/orgs/rosters/show.html.erb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
<div class="site-content-body tabnav-body clearfix">
3030
<div class="tabnav">
3131
<nav class="tabnav-tabs">
32-
<span id='students-tab' onclick="selectTab(this)" class="tabnav-tab selected tabnav-link" aria-current="page">All students</span>
33-
<span id='unlinked-tab' onclick="selectTab(this)" class="tabnav-tab tabnav-link">Unlinked GitHub accounts</span>
32+
<span id='students-tab' onclick="selectTab(this)" class="tabnav-tab selected tabnav-link" aria-current="page">All students <span class="Counter"><%= @roster_entries_count %></span></span>
33+
<span id='unlinked-tab' onclick="selectTab(this)" class="tabnav-tab tabnav-link">Unlinked GitHub accounts <span class="Counter"><%= @unlinked_user_ids.count %></span>
3434
</nav>
3535
</div>
3636
<span id='students-span'>

config/sidekiq.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@
1313
- [boom, 3]
1414
- [create_repository, 3]
1515
- [porter_status, 3]
16+
- [roster, 3]

0 commit comments

Comments
 (0)