Skip to content

Commit 892af4e

Browse files
authored
Merge pull request #86 from berkeley-cdss/staging
Staging
2 parents 8bce7c4 + 14ce300 commit 892af4e

6 files changed

Lines changed: 46 additions & 45 deletions

File tree

.tool-versions

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
ruby 3.3.9
1+
ruby 3.3.8

app/facades/canvas_facade.rb

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ def auth_header
8484
# Enable this to automatically parse JSON responses.
8585
# This will require some refactoring (and rebuilding VCRs/webmock)
8686
# do |faraday|
87-
# faraday.request :json
87+
# faraday.request :url_encoded # passed first, only affects request parameters.
88+
# faraday.request :json # 2nd, only affects request body
8889
# faraday.response :json, content_type: /\bjson$/
8990
# faraday.adapter Faraday.default_adapter
9091
# end
@@ -125,23 +126,41 @@ def depaginate_response(response)
125126
next_page = links.find { |page| page[:rel] == 'next' }
126127
return JSON.parse(response.body) if next_page.nil?
127128

128-
# raise CanvasAPIError, "Canvas API Error: #{response.status} - #{response.body}" if response.status != 200
129-
130129
# NOTE: Do not log the full :url as it contains an auth token (from canvas)
131130
JSON.parse(response.body) + depaginate_response(@canvas_conn.get(next_page[:url], headers: auth_header))
132131
end
133132

134133
##
135134
# Gets all courses for the authorized user.
136135
#
137-
# @return [Faraday::Response] list of the courses the user has access to.
136+
# @return [Array<Hash>] list of the Course (hashes) the user has access to.
138137
def get_all_courses
139138
depaginate_response(@canvas_conn.get('courses', {
140139
per_page: 100,
141140
'include[]': 'term'
142141
}))
143142
end
144143

144+
##
145+
# Get all enrollments for a course.
146+
#
147+
# https://ucberkeleysandbox.instructure.com/doc/api/courses.html#method.courses.users
148+
# @param [Course] A Course object.
149+
# @param [String|Array<String>] role the role to filter users by (optional).
150+
# @return [Array<Hash>] list of the Enrollment (hashes) in the course.
151+
def get_all_course_users(course, role = nil)
152+
# sigh, manually construct query string until we tweak Faraday middleware
153+
# to include :url_encoded, then use `'enrollment_type[]' : list_or_string`
154+
query_string = 'per_page=100'
155+
query_string += "&enrollment_type[]=#{role}" if role.is_a?(String) && role.present?
156+
157+
if role.is_a?(Array) && role.present? # rubocop:disable Style/IfUnlessModifier
158+
query_string += role.map { |r| "&enrollment_type[]=#{r}" }.join
159+
end
160+
161+
depaginate_response(@canvas_conn.get("courses/#{course.canvas_id}/users?#{query_string}"))
162+
end
163+
145164
##
146165
# Get all courses for which the user is an instructor.
147166
# Makes 2 API calls to Canvas, one for `teacher` and one for `ta`.

app/javascript/controllers/enrollments_controller.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import "datatables.net-responsive-bs5";
55

66
export default class extends Controller {
77
static values = { courseId: Number }
8-
8+
99
connect() {
1010
if (!DataTable.isDataTable('#enrollments-table')) {
1111
// Define a custom sorting function for the Role column
@@ -48,7 +48,7 @@ export default class extends Controller {
4848
})
4949
.then((response) => {
5050
if (!response.ok) {
51-
throw new Error("Failed to sync enrollments.");
51+
throw new Error(`Failed to sync enrollments. ${response.status} - ${response.statusText}`);
5252
}
5353
return response.json();
5454
})
@@ -61,4 +61,4 @@ export default class extends Controller {
6161
location.reload();
6262
});
6363
}
64-
}
64+
}

app/models/course.rb

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -176,42 +176,25 @@ def self.extract_date_field(assignment_data, field_name)
176176
end
177177
end
178178

179-
# Fetch users for a course from Canvas API
180-
# TODO: Replace with call to Canvas Facade
181-
def fetch_users_from_canvas(token, enrollment_type = nil)
182-
url = "#{ENV.fetch('CANVAS_URL')}/api/v1/courses/#{canvas_id}/users"
183-
response = Faraday.get(url) do |req|
184-
req.headers['Authorization'] = "Bearer #{token}"
185-
req.headers['Content-Type'] = 'application/json'
186-
req.params['enrollment_type'] = enrollment_type if enrollment_type.present?
187-
end
188-
189-
if response.success?
190-
JSON.parse(response.body)
191-
else
192-
Rails.logger.error "Failed to fetch users: #{response.status} - #{response.body}"
193-
[]
194-
end
195-
end
196-
197179
# Fetch users for a course and create/find their User and UserToCourse records
180+
# TODO: This may need to become a background job
198181
def sync_users_from_canvas(token, role)
199182
# Fetch all users for the course from Canvas
200-
users_data = fetch_users_from_canvas(token, role)
183+
canvas_users = CanvasFacade.new(token).get_all_course_users(self, role)
201184

202185
# Extract the Canvas user IDs from the fetched data
203-
current_canvas_user_ids = users_data.pluck('id')
186+
current_canvas_user_ids = canvas_users.pluck('id')
204187

188+
# Delete UserToCourse records for users no longer in the course
205189
# Find all UserToCourse records for this course and role
206190
existing_user_to_courses = UserToCourse.where(course_id: id, role: role)
207-
208-
# Delete UserToCourse records for users no longer in the course
209-
existing_user_to_courses.each do |user_to_course|
210-
user_to_course.destroy unless current_canvas_user_ids.include?(user_to_course.user.canvas_uid)
211-
end
191+
user_ids_to_remove = existing_user_to_courses.reject do |utc|
192+
current_canvas_user_ids.include?(utc.user.canvas_uid)
193+
end.map(&:id)
194+
UserToCourse.where(id: user_ids_to_remove).destroy_all if user_ids_to_remove.any?
212195

213196
# Create or update User and UserToCourse records for current users
214-
users_data.each do |user_data|
197+
canvas_users.each do |user_data|
215198
# this line skips importing a user if the api doesn't return their email
216199
# one case this is happening if the user was invited to the course but hasn't accepted
217200
next if user_data['email'].blank?
@@ -230,9 +213,9 @@ def sync_users_from_canvas(token, role)
230213
end
231214

232215
def sync_enrollments_from_canvas(token)
233-
sync_users_from_canvas(token, 'student')
234216
sync_users_from_canvas(token, 'teacher')
235217
sync_users_from_canvas(token, 'ta')
218+
sync_users_from_canvas(token, 'student')
236219
end
237220

238221
def regenerate_readonly_api_token_if_blank

spec/controllers/courses_controller_spec.rb

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -91,17 +91,14 @@
9191
before do
9292
roles = %w[teacher ta student]
9393
roles.each do |role|
94-
stub_request(:get, "#{ENV.fetch('CANVAS_URL', nil)}/api/v1/courses/456/users?enrollment_type=#{role}")
94+
stub_request(:get, "#{ENV.fetch('CANVAS_URL', nil)}/api/v1/courses/456/users")
9595
.with(
96-
headers: {
97-
'Accept' => '*/*',
98-
'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
99-
'Authorization' => 'Bearer fake_token',
100-
'Content-Type' => 'application/json',
101-
'User-Agent' => 'Faraday v2.12.2'
102-
}
103-
)
104-
.to_return(status: 200, body: '[]', headers: {})
96+
query: {
97+
'enrollment_type[]' => role,
98+
'per_page' => '100'
99+
},
100+
headers: { 'Authorization' => 'Bearer fake_token' }
101+
).to_return(status: 200, body: '[]', headers: {})
105102
end
106103
end
107104

spec/spec_helper.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
WebMock.disable_net_connect!(allow_localhost: true)
4444
end
4545

46+
config.include WebMock::API
47+
4648
config.expect_with :rspec do |expectations|
4749
# This option will default to `true` in RSpec 4. It makes the `description`
4850
# and `failure_message` of custom matchers include text for helper methods

0 commit comments

Comments
 (0)