bearcat 1.0.0 → 1.5.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. checksums.yaml +7 -0
  2. data/bearcat.gemspec +15 -5
  3. data/lib/badgrcat/api_array.rb +25 -0
  4. data/lib/badgrcat/client/methods.rb +54 -0
  5. data/lib/badgrcat/client.rb +53 -0
  6. data/lib/badgrcat/version.rb +3 -0
  7. data/lib/bearcat/api_array.rb +132 -65
  8. data/lib/bearcat/client/account_reports.rb +6 -14
  9. data/lib/bearcat/client/accounts.rb +18 -6
  10. data/lib/bearcat/client/analytics.rb +12 -0
  11. data/lib/bearcat/client/assignment_groups.rb +15 -0
  12. data/lib/bearcat/client/assignments.rb +17 -9
  13. data/lib/bearcat/client/blueprint_courses.rb +25 -0
  14. data/lib/bearcat/client/calendar_events.rb +9 -17
  15. data/lib/bearcat/client/canvas_files.rb +0 -2
  16. data/lib/bearcat/client/conferences.rb +3 -8
  17. data/lib/bearcat/client/content_exports.rb +39 -0
  18. data/lib/bearcat/client/content_migrations.rb +54 -0
  19. data/lib/bearcat/client/conversations.rb +3 -8
  20. data/lib/bearcat/client/courses.rb +25 -14
  21. data/lib/bearcat/client/custom_gradebook_columns.rb +21 -0
  22. data/lib/bearcat/client/discussions.rb +10 -4
  23. data/lib/bearcat/client/enrollments.rb +9 -25
  24. data/lib/bearcat/client/external_tools.rb +18 -0
  25. data/lib/bearcat/client/file_helper.rb +36 -30
  26. data/lib/bearcat/client/files.rb +9 -0
  27. data/lib/bearcat/client/folders.rb +24 -0
  28. data/lib/bearcat/client/graph_ql.rb +17 -0
  29. data/lib/bearcat/client/group_categories.rb +18 -0
  30. data/lib/bearcat/client/group_memberships.rb +14 -0
  31. data/lib/bearcat/client/groups.rb +10 -2
  32. data/lib/bearcat/client/learning_outcomes.rb +17 -0
  33. data/lib/bearcat/client/logins.rb +20 -0
  34. data/lib/bearcat/client/module_items.rb +18 -0
  35. data/lib/bearcat/client/modules.rb +12 -7
  36. data/lib/bearcat/client/o_auth2.rb +18 -9
  37. data/lib/bearcat/client/outcome_groups.rb +2 -4
  38. data/lib/bearcat/client/outcome_imports.rb +48 -0
  39. data/lib/bearcat/client/outcomes.rb +4 -7
  40. data/lib/bearcat/client/pages.rb +15 -0
  41. data/lib/bearcat/client/progresses.rb +9 -0
  42. data/lib/bearcat/client/quizzes.rb +13 -9
  43. data/lib/bearcat/client/reports.rb +37 -17
  44. data/lib/bearcat/client/roles.rb +15 -0
  45. data/lib/bearcat/client/rubric.rb +17 -0
  46. data/lib/bearcat/client/rubric_assessment.rb +13 -0
  47. data/lib/bearcat/client/rubric_association.rb +13 -0
  48. data/lib/bearcat/client/search.rb +9 -0
  49. data/lib/bearcat/client/sections.rb +10 -17
  50. data/lib/bearcat/client/sis_imports.rb +6 -12
  51. data/lib/bearcat/client/submissions.rb +53 -21
  52. data/lib/bearcat/client/tabs.rb +12 -0
  53. data/lib/bearcat/client/users.rb +32 -13
  54. data/lib/bearcat/client.rb +111 -45
  55. data/lib/bearcat/client_module.rb +103 -0
  56. data/lib/bearcat/rate_limiting/increment_bucket.lua +33 -0
  57. data/lib/bearcat/rate_limiting/redis_script.rb +164 -0
  58. data/lib/bearcat/rate_limiting.rb +69 -0
  59. data/lib/bearcat/redis_connection.rb +106 -0
  60. data/lib/bearcat/spec_helpers.rb +125 -0
  61. data/lib/bearcat/version.rb +1 -1
  62. data/lib/bearcat.rb +49 -0
  63. data/lib/catalogcat/api_array.rb +22 -0
  64. data/lib/catalogcat/client/catalogs.rb +21 -0
  65. data/lib/catalogcat/client/certificates.rb +17 -0
  66. data/lib/catalogcat/client/courses.rb +25 -0
  67. data/lib/catalogcat/client/email_domain_sets.rb +17 -0
  68. data/lib/catalogcat/client/enrollments.rb +25 -0
  69. data/lib/catalogcat/client/orders.rb +13 -0
  70. data/lib/catalogcat/client/user_registrations.rb +9 -0
  71. data/lib/catalogcat/client.rb +26 -0
  72. data/lib/catalogcat/version.rb +3 -0
  73. data/lib/catalogcat.rb +14 -0
  74. data/spec/bearcat/api_array_spec.rb +112 -0
  75. data/spec/bearcat/client/accounts_spec.rb +71 -1
  76. data/spec/bearcat/client/analytics_spec.rb +22 -0
  77. data/spec/bearcat/client/assignment_groups_spec.rb +47 -0
  78. data/spec/bearcat/client/assignments_spec.rb +43 -0
  79. data/spec/bearcat/client/blueprint_courses_spec.rb +43 -0
  80. data/spec/bearcat/client/canvas_files_spec.rb +1 -2
  81. data/spec/bearcat/client/content_exports_spec.rb +68 -0
  82. data/spec/bearcat/client/content_migrations_spec.rb +94 -0
  83. data/spec/bearcat/client/courses_spec.rb +81 -2
  84. data/spec/bearcat/client/custom_gradebook_columns_spec.rb +66 -0
  85. data/spec/bearcat/client/discussions_spec.rb +73 -0
  86. data/spec/bearcat/client/enrollments_spec.rb +10 -0
  87. data/spec/bearcat/client/external_tools_spec.rb +106 -0
  88. data/spec/bearcat/client/files_spec.rb +15 -0
  89. data/spec/bearcat/client/folders_spec.rb +18 -0
  90. data/spec/bearcat/client/graph_ql_spec.rb +35 -0
  91. data/spec/bearcat/client/group_categories_spec.rb +45 -0
  92. data/spec/bearcat/client/group_membership_spec.rb +14 -0
  93. data/spec/bearcat/client/group_memberships_spec.rb +36 -0
  94. data/spec/bearcat/client/groups_spec.rb +46 -0
  95. data/spec/bearcat/client/learning_outcomes_spec.rb +25 -0
  96. data/spec/bearcat/client/module_items_spec.rb +60 -0
  97. data/spec/bearcat/client/modules_spec.rb +38 -1
  98. data/spec/bearcat/client/o_auth2_spec.rb +3 -3
  99. data/spec/bearcat/client/pages_spec.rb +17 -0
  100. data/spec/bearcat/client/quizzes_spec.rb +41 -4
  101. data/spec/bearcat/client/reports_spec.rb +40 -1
  102. data/spec/bearcat/client/roles_spec.rb +24 -0
  103. data/spec/bearcat/client/rubric_assessment_spec.rb +47 -0
  104. data/spec/bearcat/client/rubric_association_spec.rb +39 -0
  105. data/spec/bearcat/client/rubric_spec.rb +45 -0
  106. data/spec/bearcat/client/search_spec.rb +16 -0
  107. data/spec/bearcat/client/sections_spec.rb +12 -0
  108. data/spec/bearcat/client/submissions_spec.rb +47 -2
  109. data/spec/bearcat/client/users_spec.rb +43 -0
  110. data/spec/bearcat/client_spec.rb +1 -4
  111. data/spec/bearcat/rate_limiting_spec.rb +62 -0
  112. data/spec/bearcat/stub_bearcat_spec.rb +15 -0
  113. data/spec/fixtures/access_token.json +3 -0
  114. data/spec/fixtures/account_admin_create.json +14 -0
  115. data/spec/fixtures/account_admin_delete.json +15 -0
  116. data/spec/fixtures/account_admins.json +54 -0
  117. data/spec/fixtures/account_courses.json +48 -0
  118. data/spec/fixtures/account_grading_standards.json +20 -0
  119. data/spec/fixtures/account_groups.json +42 -0
  120. data/spec/fixtures/account_role.json +34 -0
  121. data/spec/fixtures/account_roles.json +35 -0
  122. data/spec/fixtures/account_sis_imports.json +39 -0
  123. data/spec/fixtures/account_sub_accounts.json +17 -0
  124. data/spec/fixtures/accounts.json +13 -0
  125. data/spec/fixtures/assignment.json +32 -0
  126. data/spec/fixtures/assignment_group.json +7 -0
  127. data/spec/fixtures/assignment_groups.json +16 -0
  128. data/spec/fixtures/blueprint_migration.json +12 -0
  129. data/spec/fixtures/blueprint_subscriptions.json +5 -0
  130. data/spec/fixtures/blueprint_template.json +7 -0
  131. data/spec/fixtures/blueprint_update_assocations_success.json +3 -0
  132. data/spec/fixtures/communication_channels.json +10 -0
  133. data/spec/fixtures/content_export.json +9 -0
  134. data/spec/fixtures/content_migration_files/content_migration.json +13 -0
  135. data/spec/fixtures/course_copy.json +18 -0
  136. data/spec/fixtures/course_files.json +38 -0
  137. data/spec/fixtures/course_folder.json +21 -0
  138. data/spec/fixtures/course_folders.json +44 -0
  139. data/spec/fixtures/course_grading_standards.json +20 -0
  140. data/spec/fixtures/course_settings.json +33 -0
  141. data/spec/fixtures/create_course_discussion.json +44 -0
  142. data/spec/fixtures/created_group.json +37 -0
  143. data/spec/fixtures/created_group_category.json +15 -0
  144. data/spec/fixtures/created_group_membership.json +8 -0
  145. data/spec/fixtures/created_module.json +13 -0
  146. data/spec/fixtures/custom_gradebook_columns/column_data.json +4 -0
  147. data/spec/fixtures/custom_gradebook_columns/custom_gradebook_column.json +7 -0
  148. data/spec/fixtures/custom_gradebook_columns/custom_gradebook_columns.json +16 -0
  149. data/spec/fixtures/custom_gradebook_columns/gradebook_column_progress.json +14 -0
  150. data/spec/fixtures/dashboard.json +6 -0
  151. data/spec/fixtures/delete_course.json +3 -0
  152. data/spec/fixtures/delete_group_category.json +3 -0
  153. data/spec/fixtures/deleted_group.json +37 -0
  154. data/spec/fixtures/department_level_participation.json +73 -0
  155. data/spec/fixtures/department_level_statistics.json +10 -0
  156. data/spec/fixtures/discussion_entries.json +21 -0
  157. data/spec/fixtures/discussion_entry_replies.json +21 -0
  158. data/spec/fixtures/discussion_topic.json +49 -0
  159. data/spec/fixtures/discussion_topics.json +51 -0
  160. data/spec/fixtures/edited_group.json +129 -0
  161. data/spec/fixtures/edited_group_category.json +15 -0
  162. data/spec/fixtures/enrollment_terms.json +1 -1
  163. data/spec/fixtures/external_tool.json +55 -0
  164. data/spec/fixtures/external_tools.json +57 -0
  165. data/spec/fixtures/file.csv +5 -0
  166. data/spec/fixtures/gradebook_history.json +52 -0
  167. data/spec/fixtures/graph_ql_scores.json +33 -0
  168. data/spec/fixtures/group.json +15 -0
  169. data/spec/fixtures/group_categories.json +28 -0
  170. data/spec/fixtures/group_category.json +13 -0
  171. data/spec/fixtures/group_category_groups.json +20 -0
  172. data/spec/fixtures/group_membership.json +7 -0
  173. data/spec/fixtures/learning_outcome.json +32 -0
  174. data/spec/fixtures/merge_user.json +8 -0
  175. data/spec/fixtures/module.json +15 -0
  176. data/spec/fixtures/module_item.json +15 -0
  177. data/spec/fixtures/module_items.json +47 -0
  178. data/spec/fixtures/ok.json +3 -0
  179. data/spec/fixtures/outcome_result.json +13 -0
  180. data/spec/fixtures/pages.json +40 -0
  181. data/spec/fixtures/progress.json +13 -0
  182. data/spec/fixtures/quizzes/course_quiz_questions.json +59 -0
  183. data/spec/fixtures/quizzes/quiz_assignment_override.json +31 -0
  184. data/spec/fixtures/reactivate_enrollment.json +20 -0
  185. data/spec/fixtures/rubric.json +13 -0
  186. data/spec/fixtures/rubric_assessment.json +32 -0
  187. data/spec/fixtures/rubric_association.json +13 -0
  188. data/spec/fixtures/search_find_recipients.json +10 -0
  189. data/spec/fixtures/update_section.json +1 -1
  190. data/spec/fixtures/user_details.json +16 -0
  191. data/spec/fixtures/user_logins.json +9 -0
  192. data/spec/helper.rb +2 -0
  193. metadata +336 -43
@@ -0,0 +1,48 @@
1
+ module Bearcat
2
+ class Client < Footrest::Client
3
+ module OutcomeImports
4
+
5
+ def import_outcomes(file_path, params={})
6
+ params = params.with_indifferent_access
7
+ params['attachment'] = Faraday::UploadIO.new(file_path, 'text/csv')
8
+ url = "api/v1/#{outcome_import_context_slug(params)}"
9
+ url += "group/#{params[:group]}/" if params[:group].present?
10
+ params.delete(:group)
11
+ post(url, params)
12
+ end
13
+
14
+ def outcome_import_status(id, params={})
15
+ params = params.with_indifferent_access
16
+ get("api/v1/#{outcome_import_context_slug(params)}#{id}", params)
17
+ end
18
+
19
+ def outcome_import_created_group_ids(id, params={})
20
+ params = params.with_indifferent_access
21
+ get("api/v1/#{outcome_import_context_slug(params)}#{id}/created_group_ids", params)
22
+ end
23
+
24
+ protected
25
+
26
+ def outcome_import_context_slug(params)
27
+ context_hash = params.select { |k, _| k == "account" || k == "course" }
28
+
29
+ if context_hash.keys.count > 1
30
+ raise ArgumentError, "cannot have account and course in params"
31
+ elsif context_hash.empty?
32
+ "accounts/self/outcome_imports/"
33
+ else
34
+ context_hash_key = context_hash.keys.first
35
+ case context_hash_key
36
+ when 'account'
37
+ params.delete(:account)
38
+ "accounts/#{context_hash[context_hash_key]}/outcome_imports/"
39
+ when 'course'
40
+ params.delete(:course)
41
+ "courses/#{context_hash[context_hash_key]}/outcome_imports/"
42
+ end
43
+ end
44
+ end
45
+
46
+ end
47
+ end
48
+ end
@@ -1,15 +1,12 @@
1
1
  module Bearcat
2
2
  class Client < Footrest::Client
3
3
  module Outcomes
4
+ extend ClientModule
4
5
 
5
- def show_outcome(id)
6
- get("/api/v1/outcomes/#{id}")
6
+ prefix "/api/v1/outcomes/:outcome/" do
7
+ get :show_outcome
8
+ put :update_outcome
7
9
  end
8
-
9
- def update_outcome(id, params={})
10
- put("/api/v1/outcomes/#{id}", params)
11
- end
12
-
13
10
  end
14
11
  end
15
12
  end
@@ -0,0 +1,15 @@
1
+ module Bearcat
2
+ class Client < Footrest::Client
3
+ module Pages
4
+ extend ClientModule
5
+
6
+ context_types %i[course group] do |ct|
7
+ prefix "/api/v1/#{ct}s/:#{ct}/pages/" do
8
+ get :"list_#{ct}_pages"
9
+ post :"add_#{ct}_page"
10
+ get :"show_#{ct}_page", ":url"
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ module Bearcat
2
+ class Client < Footrest::Client
3
+ module Progresses
4
+ extend ClientModule
5
+
6
+ get :progress, "/api//v1/progress/:progress"
7
+ end
8
+ end
9
+ end
@@ -1,17 +1,21 @@
1
1
  module Bearcat
2
2
  class Client < Footrest::Client
3
3
  module Quizzes
4
+ extend ClientModule
4
5
 
5
- def list_course_quizzes(course, params={})
6
- get("/api/v1/courses/#{course}/quizzes", params)
7
- end
8
-
9
- def quiz(course, quiz, params={})
10
- get("/api/v1/courses/#{course}/quizzes/#{quiz}", params)
11
- end
6
+ prefix "/api/v1/courses/:course/" do
7
+ get :list_course_quizzes, "quizzes"
8
+ post :create_quiz, "quizzes"
9
+ get :quiz_assignment_overrides, "quizzes/assignment_overrides"
12
10
 
13
- def quiz_extensions(course, params={})
14
- post("/api/v1/courses/#{course}/quiz_extensions", params)
11
+ prefix "quizzes/:quiz/" do
12
+ get :quiz
13
+ put :edit_quiz
14
+ post :quiz_extensions, "extensions"
15
+ get :quiz_questions, "questions"
16
+ post :create_quiz_report, "reports"
17
+ get :get_quiz_report, "reports/:report"
18
+ end
15
19
  end
16
20
  end
17
21
  end
@@ -1,27 +1,47 @@
1
+ require 'csv'
2
+
1
3
  module Bearcat
2
4
  class Client < Footrest::Client
3
5
  module Reports
6
+ extend ClientModule
4
7
 
5
- def report_list(account)
6
- get("/api/v1/accounts/#{account}/reports")
7
- end
8
-
9
- def start_report(account, report_name, params = {})
10
- post("/api/v1/accounts/#{account}/reports/#{report_name}", params)
11
- end
12
-
13
- def report_history(account, report_name)
14
- get("/api/v1/accounts/#{account}/reports/#{report_name}")
15
- end
16
-
17
- def report_status(account, report_name, report_id)
18
- get("/api/v1/accounts/#{account}/reports/#{report_name}/#{report_id}")
8
+ prefix "/api/v1/accounts/:account/reports/" do
9
+ get :report_list
10
+ post :start_report, ":report_name"
11
+ get :report_history, ":report_name"
12
+ get :report_status, ":report_name/:report_id"
13
+ delete :delete_report, ":report_name/:report_id"
19
14
  end
20
15
 
21
- def delete_report(account, report_name, report_id)
22
- delete("/api/v1/accounts/#{account}/reports/#{report_name}/#{report_id}")
16
+ def download_report(url, save_location=nil)
17
+ #This method takes the verified URL returned in a Canvas report (attachment['url']), and if
18
+ #a save_location is included, it will download it in chunks to the disk to save memory. You
19
+ #can also download the report to memory if you do not include a save location.
20
+ attempts = 0
21
+ begin
22
+ uri = URI.parse(url)
23
+ http = Net::HTTP.new(uri.host, uri.port)
24
+ http.use_ssl = true if url.start_with?('https')
25
+ response = http.head(uri.to_s)
26
+ url = response['Location']
27
+ attempts += 1
28
+ end while attempts <= 5 && (response.code == '301' || response.code == '302' || response.code == '307')
29
+ http = Net::HTTP.new(uri.host, uri.port)
30
+ http.use_ssl = true if uri.to_s.start_with?('https')
31
+ if save_location
32
+ File.open(save_location, 'wb') do |file|
33
+ http.request_get(uri.to_s) do |resp|
34
+ resp.read_body do |segment|
35
+ file.write(segment)
36
+ end
37
+ end
38
+ end
39
+ else
40
+ response = http.request_get(uri.to_s)
41
+ CSV.parse(response.read_body, headers: true)
42
+ end
23
43
  end
24
44
 
25
45
  end
26
46
  end
27
- end
47
+ end
@@ -0,0 +1,15 @@
1
+ module Bearcat
2
+ class Client < Footrest::Client
3
+ module Roles
4
+ extend ClientModule
5
+
6
+ def roles(account_id='self', params={})
7
+ get("/api/v1/accounts/#{account_id}/roles", params)
8
+ end
9
+
10
+ def role(role_id, account_id='self', params={})
11
+ get("/api/v1/accounts/#{account_id}/roles/#{role_id}", params)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ module Bearcat
2
+ class Client < Footrest::Client
3
+ module Rubric
4
+ extend ClientModule
5
+
6
+ prefix "/api/v1/courses/:course/rubrics/" do
7
+ get :course_rubric, ":rubric"
8
+ get :course_rubrics
9
+ post :create_course_rubric
10
+ put :update_course_rubric, ":rubric"
11
+ delete :delete_course_rubric, ":rubric"
12
+ end
13
+
14
+ get :account_rubrics, "api/v1/accounts/:account/rubrics"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,13 @@
1
+ module Bearcat
2
+ class Client < Footrest::Client
3
+ module RubricAssessment
4
+ extend ClientModule
5
+
6
+ prefix "/api/v1/courses/:course/rubric_associations/:rubric_association/rubric_assessments/" do
7
+ post :create_course_rubric_assessment
8
+ put :update_course_rubric_assessment, ":rubric_assessment"
9
+ delete :delete_course_rubric_assessment, ":rubric_assessment"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Bearcat
2
+ class Client < Footrest::Client
3
+ module RubricAssociation
4
+ extend ClientModule
5
+
6
+ prefix "/api/v1/courses/:course/rubric_associations/" do
7
+ post :create_course_rubric_association
8
+ put :update_course_rubric_association, ":rubric_association"
9
+ delete :delete_course_rubric_association, ":rubric_association"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ module Bearcat
2
+ class Client < Footrest::Client
3
+ module Search
4
+ extend ClientModule
5
+
6
+ get :find_recipients, '/api/v1/conversations/find_recipients'
7
+ end
8
+ end
9
+ end
@@ -1,27 +1,20 @@
1
1
  module Bearcat
2
2
  class Client < Footrest::Client
3
3
  module Sections
4
+ extend ClientModule
4
5
 
5
- def course_sections(course, params={})
6
- get("/api/v1/courses/#{course.to_s}/sections", params)
6
+ prefix "/api/v1/sections/:section/" do
7
+ get :section
8
+ put :update_section
9
+ delete :delete_section
10
+ post :crosslist_section, "crosslist/:new_course_id"
11
+ delete :decrosslist_section, "crosslist"
7
12
  end
8
13
 
9
- def section(section)
10
- get("/api/v1/sections/#{section.to_s}")
14
+ prefix "/api/v1/courses/:course/sections/" do
15
+ get :course_sections
16
+ post :create_section
11
17
  end
12
-
13
- def create_section(course, params)
14
- post("/api/v1/courses/#{course.to_s}/sections", params)
15
- end
16
-
17
- def update_section(section, params)
18
- put("/api/v1/sections/#{section.to_s}", params)
19
- end
20
-
21
- def delete_section(section)
22
- delete("/api/v1/sections/#{section.to_s}")
23
- end
24
-
25
18
  end
26
19
  end
27
20
  end
@@ -1,20 +1,14 @@
1
1
  module Bearcat
2
2
  class Client < Footrest::Client
3
3
  module SisImports
4
-
5
- def sis_imports(account, params = {})
6
- post("/api/v1/accounts/#{account}/sis_imports", params)
7
- end
8
-
9
- def get_sis_imports(account)
10
- binding.pry
11
- get("/api/v1/accounts/#{account}/sis_imports")
4
+ def import_sis_data(account, file, options = {}, content_type: nil)
5
+ options['attachment'] = Faraday::UploadIO.new(file, content_type || "application/zip")
6
+ post("/api/v1/accounts/#{account}/sis_imports", options)
12
7
  end
13
8
 
14
- def import_status(account, import_id)
15
- get("/api/v1/accounts/#{account}/sis_imports/#{import_id}")
9
+ def sis_import_status(account, sis_id, options = {})
10
+ get("/api/v1/accounts/#{account}/sis_imports/#{sis_id}", options)
16
11
  end
17
-
18
12
  end
19
13
  end
20
- end
14
+ end
@@ -1,37 +1,69 @@
1
1
  module Bearcat
2
2
  class Client < Footrest::Client
3
3
  module Submissions
4
+ extend ClientModule
4
5
 
5
- def course_submissions(course, params={})
6
- get("/api/v1/courses/#{course.to_s}/students/submissions", params)
6
+ prefix "/api/v1/courses/:course/assignments/:assignment/submissions/" do
7
+ get :get_course_submissions
8
+ get :user_course_assignment_submission, ":user"
7
9
  end
8
10
 
9
- def course_submission(course, assignment, params)
10
- post("/api/v1/courses/#{course}/assignments/#{assignment}/submissions", params)
11
- end
11
+ context_types %i[course section] do |ct|
12
+ prefix "/api/v1/#{ct}s/:#{ct}/" do
13
+ get :"#{ct}_submissions", "students/submissions"
12
14
 
13
- def section_submission(section, assignment, params)
14
- post("/api/v1/sections/#{section}/assignments/#{assignment}/submissions", params)
15
+ prefix "assignments/:assignment/submissions/" do
16
+ post :"#{ct}_submission"
17
+ put :"grade_#{ct}_submission", ":user"
18
+ post :"#{ct}_update_grades", "update_grades"
19
+ end
20
+ end
15
21
  end
16
22
 
17
- def course_file_upload_submission(course, assignment, user, file_path, params={})
18
- response = upload_file("/api/v1/courses/#{course}/assignments/#{assignment}/submissions/#{user}/files", file_path, params)
19
- params['submission'] = {
20
- 'submission_type' => 'online_upload',
21
- 'file_ids'=> [response['id']]
22
- }
23
- course_submission(course, assignment, params)
23
+ def course_file_upload_submission(course, assignment, user, file_data, params={})
24
+ file_upload_submission(course, assignment, user, file_data, params, type: :course)
24
25
  end
25
26
 
26
- def section_file_upload_submission(section, assignment, user, file_path, params={})
27
- response = upload_file("/api/v1/sections/#{section}/assignments/#{assignment}/submissions/#{user}/files", file_path, params)
28
- params['submission'] = {
29
- 'submission_type' => 'online_upload',
30
- 'file_ids'=> [response['id']]
31
- }
32
- section_submission(section, assignment, params)
27
+ def section_file_upload_submission(section, assignment, user, file_data, params={})
28
+ file_upload_submission(section, assignment, user, file_data, params, type: :section)
33
29
  end
34
30
 
31
+ protected
32
+
33
+ # @param file_data One of an array of file_path strings or an array of Hashes, each being upload file params plus the file's path
34
+ def file_upload_submission(type_id, assignment, user, file_data=nil, params={}, type:)
35
+ raise ArgumentError, 'Invalid type' unless [:course, :section].include?(type)
36
+ raise ArgumentError, 'Must provide either file paths or fully formed submission params' if params.dig(:submission, :file_ids).nil? && file_data.nil?
37
+
38
+ params = params.with_indifferent_access
39
+ sub_params = if params.dig(:submission, :file_ids).present?
40
+ {}
41
+ else
42
+ file_data = Array.wrap(file_data)
43
+ file_ids = file_data.map do |datum|
44
+ if datum.is_a?(Hash) && datum[:file_path].present?
45
+ # datum is a param hash for upload_file API plus the file_path
46
+ path = datum[:file_path]
47
+ upload_params = datum.except(:file_path)
48
+ else
49
+ # treat datum as a file_path string
50
+ path = datum
51
+ upload_params = params
52
+ end
53
+
54
+ response = upload_file("/api/v1/#{type}s/#{type_id}/assignments/#{assignment}/submissions/#{user}/files", path, upload_params)
55
+ response['id']
56
+ end
57
+
58
+ {
59
+ submission_type: 'online_upload',
60
+ file_ids: file_ids,
61
+ }
62
+ end
63
+ sub_params.merge!(params[:submission]) if params[:submission].present?
64
+ params[:submission] = sub_params
65
+ send("#{type}_submission".to_sym, type_id, assignment, params)
66
+ end
35
67
  end
36
68
  end
37
69
  end
@@ -0,0 +1,12 @@
1
+ module Bearcat
2
+ class Client < Footrest::Client
3
+ module Tabs
4
+ extend ClientModule
5
+
6
+ prefix "/api/v1/courses/:course/tabs/" do
7
+ get :get_tabs
8
+ put :update_tab, ":tab"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -1,21 +1,34 @@
1
1
  module Bearcat
2
2
  class Client < Footrest::Client
3
3
  module Users
4
+ extend ClientModule
4
5
 
5
- def list_users(account, params={})
6
- get("/api/v1/accounts/#{account.to_s}/users", params)
6
+ prefix "/api/v1/accounts/:account/users/" do
7
+ get :list_users
8
+ post :add_user
7
9
  end
8
10
 
9
- def user_avatars(user, params={})
10
- get("/api/v1/users/#{user.to_s}/avatars", params)
11
- end
12
-
13
- def add_user(account, params={})
14
- post("/api/v1/accounts/#{account.to_s}/users", params)
15
- end
11
+ prefix "/api/v1/users/" do
12
+ prefix ":user/" do
13
+ get :user_detail
14
+ get :user_avatars, "avatars"
15
+ get :user_profile, "profile"
16
+ get :user_logins, "logins"
17
+ get :communication_channels, "communication_channels"
18
+ get :page_views, "page_views"
19
+ put :user_merge, "merge_into/:merge_into_user"
20
+ get :user_assignments, "courses/:course/assignments"
21
+ get :dashboard_positions, "dashboard_positions"
22
+ put :update_dashboard_positions, "dashboard_positions"
23
+ end
16
24
 
17
- def user_profile(user)
18
- get("/api/v1/users/#{user.to_s}/profile")
25
+ prefix "self/" do
26
+ get :course_nicknames, "course_nicknames"
27
+ get :course_nickname, "course_nicknames/:course"
28
+ put :set_course_nickname, "course_nicknames/:course"
29
+ delete :delete_course_nickname, "course_nicknames/:course"
30
+ delete :clear_course_nickname, "course_nicknames"
31
+ end
19
32
  end
20
33
 
21
34
  # scope: food
@@ -40,8 +53,14 @@ module Bearcat
40
53
  delete("/api/v1/users/#{user}/custom_data/#{scope}", params)
41
54
  end
42
55
 
43
- def page_views(user, params = {})
44
- get("/api/v1/users/#{user}/page_views", params)
56
+ def favorite_courses(user, params = {})
57
+ params.merge!({as_user_id: user})
58
+ get("/api/v1/users/self/favorites/courses", params)
59
+ end
60
+
61
+ def unfavorite_course(user, course, params = {})
62
+ params.merge!({as_user_id: user})
63
+ delete("/api/v1/users/self/favorites/courses/#{course}", params)
45
64
  end
46
65
  end
47
66
  end
@@ -1,54 +1,120 @@
1
+ require 'active_support/core_ext/hash'
1
2
  require 'footrest/client'
3
+ require_relative 'rate_limiting'
4
+ require_relative 'client_module'
5
+ require_relative 'redis_connection'
6
+
2
7
  module Bearcat
3
8
  class Client < Footrest::Client
4
9
  require 'bearcat/api_array'
5
- require 'bearcat/client/file_helper'
6
- require 'bearcat/client/assignments'
7
- require 'bearcat/client/courses'
8
- require 'bearcat/client/enrollments'
9
- require 'bearcat/client/outcome_groups'
10
- require 'bearcat/client/outcomes'
11
- require 'bearcat/client/sections'
12
- require 'bearcat/client/o_auth2'
13
- require 'bearcat/client/groups'
14
- require 'bearcat/client/conferences'
15
- require 'bearcat/client/users'
16
- require 'bearcat/client/reports'
17
- require 'bearcat/client/accounts'
18
- require 'bearcat/client/submissions'
19
- require 'bearcat/client/conversations'
20
- require 'bearcat/client/modules'
21
- require 'bearcat/client/canvas_files'
22
- require 'bearcat/client/calendar_events'
23
- require 'bearcat/client/discussions'
24
- require 'bearcat/client/quizzes'
25
-
26
- include Assignments
27
- include Accounts
28
- include Courses
29
- include Enrollments
30
- include OutcomeGroups
31
- include Outcomes
32
- include Sections
33
- include OAuth2
34
- include Groups
35
- include Conferences
36
- include Users
37
- include Reports
38
- include Submissions
39
- include Conversations
40
- include Modules
41
- include CanvasFiles
42
- include CalendarEvents
43
- include Discussions
44
- include FileHelper
45
- include Quizzes
46
-
47
-
48
- # Override Footrest request for ApiArray support
10
+
11
+ @added_modules = []
12
+
13
+ Dir[File.join(__dir__, 'client', '*.rb')].each do |mod|
14
+ mname = File.basename(mod, '.*').camelize
15
+ mname = 'GraphQL' if mname == 'GraphQl'
16
+ require mod
17
+ lmod = "Bearcat::Client::#{mname}".constantize
18
+ include lmod
19
+ @added_modules << lmod
20
+ end
21
+
22
+ def self.registered_endpoints
23
+ @registered_endpoints ||= @added_modules.reduce({}) do |h, m|
24
+ h.merge!(m._registered_endpoints) rescue h
25
+ end
26
+ @registered_endpoints
27
+ end
28
+
49
29
  def request(method, &block)
50
- ApiArray::process_response(connection.send(method, &block), self)
30
+ response = rate_limited_request do
31
+ connection.send(method, &block)
32
+ end
33
+ ApiArray.process_response(response, self)
34
+ end
35
+
36
+ def set_connection(config)
37
+ super
38
+ connection.builder.insert(Footrest::RaiseFootrestErrors, ExtendedRaiseFootrestErrors)
39
+ connection.builder.delete(Footrest::RaiseFootrestErrors)
40
+ end
41
+
42
+ protected
43
+
44
+ def rate_limited_request
45
+ return yield unless rate_limiter
46
+
47
+ canvas_rate_limits= 0
48
+ response = nil
49
+
50
+ begin
51
+ rate_limiter.apply(
52
+ rate_limit_key,
53
+ max_sleep: Bearcat.max_sleep_seconds,
54
+ on_sleep: ->(tts) {
55
+ message = "Canvas API rate limit reached; sleeping for #{tts.to_i} second(s) to catch up."
56
+ Bearcat.logger.debug(message)
57
+ },
58
+ ) do
59
+ response = yield
60
+ 0
61
+ end
62
+ rescue Footrest::HttpError::Forbidden => err
63
+ # Somehow our rate-limiting didn't limit enough and Canvas stopped us.
64
+ response = err.response
65
+ if canvas_rate_limits < 2 && err.message.include?("(Rate Limit Exceeded)")
66
+ canvas_rate_limits += 1
67
+ rate_limiter.checkin_known(rate_limit_key, 0)
68
+
69
+ message = "Canvas API applied rate limit; upticking Bearcat rate-limit avoidance and retrying (Retry #{canvas_rate_limits})."
70
+ Bearcat.logger.debug(message)
71
+
72
+ retry
73
+ end
74
+ raise
75
+ ensure
76
+ headers = response.try(:response_headers) || response.try(:headers) || {}
77
+ # -50 to provide a little extra leeway and hopefully be more proactive, making sure we don't even get close to Canvas throwing a 403, even if an out-of-band process is involved
78
+ rate_limiter.checkin_known(rate_limit_key, headers['x-rate-limit-remaining'].to_f - 100) if response
79
+ end
80
+
81
+ response
82
+ end
83
+
84
+ def rate_limiter
85
+ @rate_limiter ||= begin
86
+ rl = config[:rate_limiter] || Bearcat.rate_limiter
87
+ master_rate_limit = config[:master_rate_limit].present? ? config[:master_rate_limit] : Bearcat.master_rate_limit
88
+
89
+ if rl.nil? && master_rate_limit.nil? && defined?(Rails) && Rails.env.production? && defined?(::Redis) && Bearcat::RedisConnection.configured?("BEARCAT", explicit: false)
90
+ master_rate_limit = true
91
+ end
92
+
93
+ if rl.nil? && master_rate_limit
94
+ rl = RateLimiting::RedisLimiter
95
+ end
96
+
97
+ if rl.is_a?(Class)
98
+ rl.new()
99
+ elsif rl.present?
100
+ rl
101
+ end
102
+ end
103
+ end
104
+
105
+ private
106
+
107
+ def rate_limit_key
108
+ Digest::SHA1.hexdigest(config[:token])
51
109
  end
52
110
  end
53
111
 
112
+ # Overridden response error middleware that, if an error code doesn't map to an exception, raises a more generic exception
113
+ class ExtendedRaiseFootrestErrors < Footrest::RaiseFootrestErrors
114
+ def on_complete(response)
115
+ super
116
+ key = response[:status].to_i
117
+ raise ERROR_MAP[key.floor(-2)], response if key >= 400
118
+ end
119
+ end
54
120
  end