Skip to content

Commit d58ec62

Browse files
committed
OutcomesImportApiController - Add implementation for stop gap solution
Fixes CNVS-20408 Fixes CNVS-21260 Test Plan: NOTE: You will need to have the Academic Benchmarks API key, and you will need to run this from an IP address inside the Instructure network * As a site-admin, make the following requests and verify that they work. Do the same as a regular account admin, a teacher, and a student, and verify that those DO NOT work (should return 401 unauthorized - Through the API, query for available GUIDs to import - Get to "/api/v1/accounts/#{@account.id}/outcomes_import/available" - Through the API, schedule a GUID for importing - Post to "/api/v1/accounts/#{@account.id}/outcomes_import" with guid as argument Change-Id: Ia7f199416f46da2aae4a356d5fa6c3c1d5845bfa Reviewed-on: https://gerrit.instructure.com/54648 Tested-by: Jenkins Reviewed-by: Cameron Sutter <csutter@instructure.com> Product-Review: Cameron Sutter <csutter@instructure.com> QA-Review: Michael Hargiss <mhargiss@instructure.com>
1 parent e920f24 commit d58ec62

10 files changed

Lines changed: 1115 additions & 8 deletions

File tree

app/controllers/application_controller.rb

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,11 +439,26 @@ def require_context
439439
end
440440

441441
def require_context_and_read_access
442-
return require_context && authorized_action(@context, @current_user, :read)
442+
require_context && authorized_action(@context, @current_user, :read)
443443
end
444444

445445
helper_method :clean_return_to
446446

447+
def require_account_context
448+
require_context_type(Account)
449+
end
450+
451+
def require_course_context
452+
require_context_type(Course)
453+
end
454+
455+
def require_context_type(klass)
456+
unless require_context && @context.is_a?(klass)
457+
raise ActiveRecord::RecordNotFound.new("Context must be of type '#{klass}'")
458+
end
459+
true
460+
end
461+
447462
MAX_ACCOUNT_LINEAGE_TO_SHOW_IN_CRUMBS = 3
448463

449464
# Can be used as a before_filter, or just called from controller code.
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
#
2+
# Copyright (C) 2012 Instructure, Inc.
3+
#
4+
# This file is part of Canvas.
5+
#
6+
# Canvas is free software: you can redistribute it and/or modify it under
7+
# the terms of the GNU Affero General Public License as published by the Free
8+
# Software Foundation, version 3 of the License.
9+
#
10+
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
11+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12+
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
13+
# details.
14+
#
15+
# You should have received a copy of the GNU Affero General Public License along
16+
# with this program. If not, see <http://www.gnu.org/licenses/>.
17+
#
18+
19+
class OutcomesImportApiController < ApplicationController
20+
include Api::V1::Outcome
21+
22+
before_filter :require_user, :require_account_context,
23+
:can_manage_global_outcomes, :has_api_config
24+
25+
def available
26+
render json: list_of_available_guids
27+
end
28+
29+
def create
30+
return render json: { error: "must specify a guid to import" } unless params[:guid]
31+
return unless valid_guid(params[:guid])
32+
33+
begin
34+
err_msg = "Import failed to queue"
35+
migration = AcademicBenchmark.import(Array(params[:guid])).first
36+
raise RuntimeError.new(err_msg) unless migration
37+
render json: { migration_id: migration.id, guid: params[:guid] }
38+
rescue StandardError => e
39+
render json: { error: "#{err_msg}: #{e.message}" }
40+
end
41+
end
42+
43+
protected
44+
45+
def can_manage_global_outcomes
46+
authorized_action(Account.site_admin, @current_user, :manage_global_outcomes)
47+
end
48+
49+
def has_api_config
50+
err = "The AcademicBenchmark API is not configured"
51+
if !AcademicBenchmark.config
52+
render json: { error: "#{err} (needs api_key and api_url)" }
53+
return false
54+
elsif !AcademicBenchmark.config["api_key"]
55+
render json: { error: "#{err} (needs api_key)" }
56+
return false
57+
elsif !AcademicBenchmark.config["api_url"]
58+
render json: { error: "#{err} (needs api_url)" }
59+
return false
60+
end
61+
true
62+
end
63+
64+
##
65+
# valid guids can only contain hex digits (letters all upper case),
66+
# and must be separated between a '-' [8-4-4-4-12]
67+
#
68+
# example: A833C528-901A-11DF-A622-0C319DFF4B22
69+
##
70+
def valid_guid(guid)
71+
unless guid =~ /[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}/
72+
render json: { error: "GUID is invalid" }
73+
return false
74+
end
75+
true
76+
end
77+
78+
##
79+
# Extract the national standards from the list of authorities
80+
# National Standards are also known as Common Core and NGSS
81+
##
82+
def nat_stds_guid(authorities)
83+
authorities.find{|a| a["title"] == "National Standards"}["guid"]
84+
end
85+
86+
def api_connection
87+
# The api credentials for accessing the Academic Benchmarks API
88+
# are stored in the database. This retrieves them
89+
config = AcademicBenchmark.config
90+
91+
# create a new api connection. Note that this does not actually
92+
# make a request to the API
93+
AcademicBenchmark::Api.new(config["api_key"], base_url: config["api_url"])
94+
end
95+
96+
##
97+
# get a list of all of the available authorities,
98+
# and sort them alphabetically by title.
99+
#
100+
# Academic Benchmarks authorities are generally State Standards,
101+
# although "National Statndards" is an authority which must be
102+
# browsed in order to retrieve specifics like NGSS and Common Core
103+
##
104+
def retrieve_authorities(api)
105+
authorities = api.list_available_authorities.select { |a| a.key?("title") }
106+
authorities.sort{ |a, b| a["title"] <=> b["title"] }
107+
end
108+
109+
def extract_common_core_and_ngss(api, nat_stds_guid)
110+
api.browse_guid(nat_stds_guid).first["itm"].first["itm"]
111+
end
112+
113+
##
114+
# Get a list of all of the available guids that users can import.
115+
# These can be passed to the `create` action
116+
##
117+
def list_of_available_guids
118+
api = api_connection
119+
auth_list = retrieve_authorities(api)
120+
121+
# prepend the common core and next gen science standards to the list
122+
auth_list.unshift(extract_common_core_and_ngss(api, nat_stds_guid(auth_list)))
123+
124+
# append the UK standards to the end of the list and flatten it down
125+
auth_list.push(uk_guid(api)).flatten
126+
end
127+
128+
# The UK standards are now available to us as well,
129+
def uk_guid(api)
130+
api.browse.find{ |a| a["title"] == "United Kingdom" }
131+
end
132+
end

config/routes.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1574,6 +1574,16 @@ def og_routes(context)
15741574
get 'courses/:course_id/outcome_results', action: :index, as: 'course_outcome_results'
15751575
end
15761576

1577+
scope(controller: :outcomes_import_api) do
1578+
# These can be uncommented when implemented
1579+
# get "global/outcomes_import", action: :index
1580+
# get "global/outcomes_import/:id", action: :show
1581+
# put "global/outcomes_import/:id", action: :cancel
1582+
# get "global/outcomes_import/list/:guid", action: :list
1583+
get "global/outcomes_import/available", action: :available
1584+
post "global/outcomes_import", action: :create
1585+
end
1586+
15771587
scope(controller: :group_categories) do
15781588
resources :group_categories, except: [:index, :create]
15791589
get 'accounts/:account_id/group_categories', action: :index, as: 'account_group_categories'

gems/plugins/academic_benchmark/lib/academic_benchmark.rb

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,23 @@ class APIError < StandardError; end
1515
def self.import(guid_or_guids)
1616
unless AcademicBenchmark.config[:api_key]
1717
puts "Not importing academic benchmark data because no API key is set"
18-
return
18+
return []
1919
end
2020

2121
# need a user with global outcome management rights
2222
user_id = Setting.get("academic_benchmark_migration_user_id", nil)
2323
unless user_id
2424
puts "Not importing academic benchmark data because no user id set"
25-
return
25+
return []
2626
end
2727

28-
if (permissionful_user = User.where(id: user_id).first)
29-
Array(guid_or_guids).each do |guid|
30-
AcademicBenchmark.queue_migration_for_guid(guid, permissionful_user)
31-
end
32-
else
28+
unless (permissionful_user = User.where(id: user_id).first)
3329
puts "Not importing academic benchmark data because no user found"
30+
return []
31+
end
32+
33+
Array(guid_or_guids).map do |guid|
34+
AcademicBenchmark.queue_migration_for_guid(guid, permissionful_user).first
3435
end
3536
end
3637

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[
2+
{
3+
"title": "United Kingdom",
4+
"guid": "e13189f3",
5+
"type": "country",
6+
"chld": "1"
7+
},
8+
{
9+
"title": "Australia",
10+
"guid": "bb2bbbb8",
11+
"type": "country",
12+
"chld": "2"
13+
},
14+
{
15+
"title": "United States",
16+
"guid": "c0d8318b",
17+
"type": "country",
18+
"chld": "52"
19+
}
20+
]
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
[
2+
{
3+
"title": "United States",
4+
"guid": "4850ad62",
5+
"type": "country",
6+
"itm": [
7+
{
8+
"title": "National Standards",
9+
"guid": "18d9f221",
10+
"type": "authority",
11+
"itm": [
12+
{
13+
"title": "Common Core State Standards",
14+
"guid": "89c8ea94",
15+
"type": "document",
16+
"chld": "2"
17+
},
18+
{
19+
"title": "NGSS Arranged by Topic",
20+
"guid": "138c9cb7",
21+
"type": "document",
22+
"chld": "1"
23+
},
24+
{
25+
"title": "Next Generation Science Standards NGSS Arranged by Disciplinary Core Idea (DCI)",
26+
"guid": "923ce7bf",
27+
"type": "document",
28+
"chld": "1"
29+
}
30+
]
31+
}
32+
]
33+
}
34+
]

0 commit comments

Comments
 (0)