panda_pal 4.1.0.beta3 → 5.0.0
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.
- checksums.yaml +5 -5
- data/README.md +60 -71
- data/app/models/panda_pal/organization.rb +46 -17
- data/db/618eef7c0380ba654ad16f867a919e72.sqlite3 +0 -0
- data/db/9ff93d4f7e0e9dc80a43f68997caf4a1.sqlite3 +0 -0
- data/db/a3fda4044a7215bc2c9eb01a4b9e517a.sqlite3 +0 -0
- data/db/daa0e6378a5ec76fcce83b7070dad219.sqlite3 +0 -0
- data/lib/panda_pal/helpers/controller_helper.rb +38 -33
- data/lib/panda_pal/version.rb +1 -1
- data/panda_pal.gemspec +0 -3
- data/spec/dummy/config/application.rb +1 -7
- data/spec/dummy/config/environments/development.rb +14 -0
- data/spec/dummy/config/environments/production.rb +11 -0
- data/spec/dummy/config/initializers/assets.rb +11 -0
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +15058 -0
- data/spec/dummy/log/test.log +0 -0
- data/spec/models/panda_pal/organization_spec.rb +89 -0
- data/spec/spec_helper.rb +0 -4
- metadata +19 -41
- data/app/models/panda_pal/organization/settings_validation.rb +0 -111
- data/app/models/panda_pal/organization/task_scheduling.rb +0 -172
- data/app/views/panda_pal/lti/iframe_cookie_fix.html.erb +0 -12
- data/spec/models/panda_pal/organization/settings_validation_spec.rb +0 -175
- data/spec/models/panda_pal/organization/task_scheduling_spec.rb +0 -144
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b728d9c41289ccb3ddeba5c13245345750900733052dd26482f1f2efc2d048a8
|
4
|
+
data.tar.gz: e5418d426bbe784e5a4ec804339a81f326d936297b6d0c20bba432c3e2d851d2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 58c36a718b50b9f320f101b3d52318ad6557cae9aa80dbb13531a798061a6243156834cc9170a452235be0b330f06772e6d571dc73fa25da2517254fc81850a7
|
7
|
+
data.tar.gz: 903d775a21f5550e0aab21593242308ff3e7ae991fc0df1467376a6a7b210b3c4fec7ae567e074595f48a254ea896f66d9770ebfb8904522cc754e2f7ac14d0b
|
data/README.md
CHANGED
@@ -28,38 +28,6 @@ Use one of these 6 options in `PandaPal.lti_options` hash.
|
|
28
28
|
5. Leave this property off, and you will get the dynamic host with the root path ('http://appdomain.com/') by default.
|
29
29
|
6. If you really do not want this property use the option `launch_url: false` for it to be left off.
|
30
30
|
|
31
|
-
### Task Scheduling
|
32
|
-
`PandaPal` includes an integration with `sidekiq-scheduler`. You can define tasks on an Organization class Stub like so:
|
33
|
-
```ruby
|
34
|
-
# <your_app>/app/models/organization.rb
|
35
|
-
module PandaPal
|
36
|
-
class Organization
|
37
|
-
# Will invoke CanvasSyncStarterWorker.perform_async() according to the cron schedule
|
38
|
-
scheduled_task '0 15 05 * * *', :identifier, worker: CanvasSyncStarterWorker
|
39
|
-
|
40
|
-
# Will invoke the method 'organization_method' on the Organization
|
41
|
-
scheduled_task '0 15 05 * * *', :organization_method_and_identifier
|
42
|
-
|
43
|
-
# If you need to invoke the same method on multiple schedules
|
44
|
-
scheduled_task '0 15 05 * * *', :identifier, worker: :organization_method
|
45
|
-
|
46
|
-
# You can also use a block
|
47
|
-
scheduled_task '0 15 05 * * *', :identifier do
|
48
|
-
# Do Stuff
|
49
|
-
end
|
50
|
-
|
51
|
-
# You can use a Proc (called in the context of the Organization) to determine the schedule
|
52
|
-
scheduled_task -> { settings[:cron] }, :identifier
|
53
|
-
|
54
|
-
# You can specify a timezone. If a TZ is not coded and settings[:timezone] is present, it will be appended automatically
|
55
|
-
scheduled_task '0 15 05 * * * America/Denver', :identifier, worker: :organization_method
|
56
|
-
|
57
|
-
# Setting settings[:task_schedules][:identifier] will override the code cron schedule. Setting it to false will disable the Task
|
58
|
-
# :identifer values _must_ be unique, but can be nil, in which case they will be determined by where (lineno etc) scheduled_task is called
|
59
|
-
end
|
60
|
-
end
|
61
|
-
```
|
62
|
-
|
63
31
|
# Organization Attributes
|
64
32
|
id: Primary Key
|
65
33
|
name: Name of the organization. Used to on requests to select the tenant
|
@@ -258,43 +226,9 @@ In your panda_pal initializer (i.e. config/initializers/panda_pal.rb or config/i
|
|
258
226
|
You can specify options that can include a structure for your settings. If specified, PandaPal will
|
259
227
|
enforce this structure on any new / updated organizations.
|
260
228
|
|
261
|
-
```ruby
|
262
|
-
PandaPal.lti_options = {
|
263
|
-
title: 'LBS Gradebook',
|
264
|
-
settings_structure: {
|
265
|
-
allow_additional: true, # Allow additional properties that aren't included in the :properties Hash.
|
266
|
-
allow_additional: { type: 'String' }, # You can also set :allow_additional to a settings specification that will be used to validate each additional setting
|
267
|
-
validate: ->(value, spec, **kwargs) {
|
268
|
-
# kwargs currently includes:
|
269
|
-
# :errors => [An array to push errors to]
|
270
|
-
# :path => [An array representation of the current path in the settings object]
|
271
|
-
|
272
|
-
# To add errors, you may:
|
273
|
-
# Push strings to the kwargs[:errors] Array:
|
274
|
-
kwargs[:errors] << "Your error message at <path>" unless value < 10
|
275
|
-
# Or return a string or string array:
|
276
|
-
value.valid? ? nil : "Your error message at <path>" # <path> will be replaced with the actual path that the error occurred at
|
277
|
-
},
|
278
|
-
properties: {
|
279
|
-
canvas_api_token: { type: 'String', required: true, },
|
280
|
-
catalog: { # :validate, :allow_additional, :properties keys are all supported at this level as well
|
281
|
-
type: 'Hash',
|
282
|
-
required: false,
|
283
|
-
validate: -> (*args) {},
|
284
|
-
allow_additional: false,
|
285
|
-
properties: {
|
286
|
-
|
287
|
-
},
|
288
|
-
}
|
289
|
-
}
|
290
|
-
},
|
291
|
-
}
|
292
|
-
```
|
293
|
-
|
294
|
-
#### Legacy Settings Structure:
|
295
229
|
Here is an example options specification:
|
296
230
|
|
297
|
-
```
|
231
|
+
```
|
298
232
|
PandaPal.lti_options = {
|
299
233
|
title: 'LBS Gradebook',
|
300
234
|
settings_structure: YAML.load("
|
@@ -347,7 +281,62 @@ Safari is weird, and you'll potentially run into issues getting `POST` requests
|
|
347
281
|
|
348
282
|
This will allow `PandaPal` to apply an iframe cookie fix that will allow CSRF validation to work.
|
349
283
|
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
284
|
+
|
285
|
+
### PandaPal 5
|
286
|
+
|
287
|
+
It has been a constant struggle to force safari to store and allow
|
288
|
+
access to a rails session while the application is embedded in Canvas.
|
289
|
+
|
290
|
+
As of PandaPal 5, a persistent session is no longer required by panda_pal.
|
291
|
+
|
292
|
+
This means that safari will likely refuse to send info about your rails session
|
293
|
+
back to the LTI, and the application will start up a new session each time the
|
294
|
+
browser navigates. This likely means a new session each time the LTI launches.
|
295
|
+
|
296
|
+
You will want to watch out for a few scenarios:
|
297
|
+
|
298
|
+
1) Make sure you are using "redirect_with_session_to" if you need to redirect
|
299
|
+
and have your PandaPal session_key persisted server side.
|
300
|
+
2) Use the "Authorization" header with "token={session_key}" to send your
|
301
|
+
PandaPal session info into api calls.
|
302
|
+
3) If you use link_to and navigate in your LTI (apps that are not single page)
|
303
|
+
make sure you include an encrypted_session_key parameter in your links.
|
304
|
+
|
305
|
+
# Upgrading from PandaPal 4 to 5:
|
306
|
+
|
307
|
+
If your tool is setup according to a pretty standard pattern (see pace_plans,
|
308
|
+
canvas_group_enrollment, etc), you shouldn't have to do anything to upgrade.
|
309
|
+
|
310
|
+
You will want to make sure that IF your launch controller is redirecting, it is
|
311
|
+
using "redirect_with_session_to".
|
312
|
+
|
313
|
+
Here is an example launch / account controller setup, assuming an account launch.
|
314
|
+
|
315
|
+
```
|
316
|
+
class LaunchController < ApplicationController
|
317
|
+
# We don't verify CSRF on launch because the LTI launch is done via a POST
|
318
|
+
# request, and Canvas wouldn't know anything about the CSRF
|
319
|
+
skip_before_action :verify_authenticity_token
|
320
|
+
skip_before_action :forbid_access_if_lacking_session # We don't have a session yet
|
321
|
+
around_action :switch_tenant
|
322
|
+
before_action :validate_launch!
|
323
|
+
before_action :handle_launch
|
324
|
+
|
325
|
+
def handle_launch
|
326
|
+
current_session_data[:canvas_user_id] = params[:custom_canvas_user_id]
|
327
|
+
current_session_data[:canvas_course_id] = params[:custom_canvas_course_id]
|
328
|
+
end
|
329
|
+
|
330
|
+
def account
|
331
|
+
redirect_with_session_to :accounts_url
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
class AccountController < ApplicationController
|
336
|
+
prepend_before_action :forbid_access_if_lacking_session
|
337
|
+
|
338
|
+
def index
|
339
|
+
end
|
340
|
+
end
|
341
|
+
```
|
342
|
+
|
@@ -1,23 +1,15 @@
|
|
1
|
-
Dir[File.dirname(__FILE__) + "/organization/*.rb"].each { |file| require file }
|
2
|
-
|
3
1
|
module PandaPal
|
4
|
-
module OrganizationConcerns; end
|
5
2
|
|
6
3
|
class Organization < ActiveRecord::Base
|
7
|
-
include OrganizationConcerns::SettingsValidation
|
8
|
-
include OrganizationConcerns::TaskScheduling if defined?(Sidekiq.schedule)
|
9
|
-
|
10
4
|
attribute :settings
|
11
|
-
serialize :settings, Hash
|
12
5
|
attr_encrypted :settings, marshal: true, key: :encryption_key
|
13
6
|
before_save {|a| a.settings = a.settings} # this is a hacky work-around to a bug where attr_encrypted is not saving settings in place
|
14
|
-
|
15
7
|
validates :key, uniqueness: { case_sensitive: false }, presence: true
|
16
8
|
validates :secret, presence: true
|
17
9
|
validates :name, uniqueness: { case_sensitive: false }, presence: true, format: { with: /\A[a-z0-9_]+\z/i }
|
18
10
|
validates :canvas_account_id, presence: true
|
19
11
|
validates :salesforce_id, presence: true, uniqueness: true
|
20
|
-
|
12
|
+
validate :validate_settings
|
21
13
|
after_create :create_schema
|
22
14
|
after_commit :destroy_schema, on: :destroy
|
23
15
|
|
@@ -25,6 +17,8 @@ module PandaPal
|
|
25
17
|
errors.add(:name, 'should not be changed after creation') if name_changed?
|
26
18
|
end
|
27
19
|
|
20
|
+
serialize :settings, Hash
|
21
|
+
|
28
22
|
def encryption_key
|
29
23
|
# production environment might not have loaded secret_key_base yet.
|
30
24
|
# In that case, just read it from env.
|
@@ -35,14 +29,6 @@ module PandaPal
|
|
35
29
|
end
|
36
30
|
end
|
37
31
|
|
38
|
-
def switch_tenant(&block)
|
39
|
-
if block_given?
|
40
|
-
Apartment::Tenant.switch(name, &block)
|
41
|
-
else
|
42
|
-
Apartment::Tenant.switch!(name)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
32
|
private
|
47
33
|
|
48
34
|
def create_schema
|
@@ -52,5 +38,48 @@ module PandaPal
|
|
52
38
|
def destroy_schema
|
53
39
|
Apartment::Tenant.drop name
|
54
40
|
end
|
41
|
+
|
42
|
+
def validate_settings
|
43
|
+
record = self
|
44
|
+
if PandaPal.lti_options && PandaPal.lti_options[:settings_structure]
|
45
|
+
validate_level(record, PandaPal.lti_options[:settings_structure], record.settings, [])
|
46
|
+
end
|
47
|
+
end
|
48
|
+
def validate_level(record, expectation, reality, previous_keys)
|
49
|
+
# Verify that the data elements at this level conform to requirements.
|
50
|
+
if expectation
|
51
|
+
expectation.each do |key, value|
|
52
|
+
is_required = expectation[key].try(:delete, :is_required)
|
53
|
+
data_type = expectation[key].try(:delete, :data_type)
|
54
|
+
value = reality.is_a?(Hash) ? reality.dig(key) : reality
|
55
|
+
|
56
|
+
if is_required && !value
|
57
|
+
record.errors[:settings] << "PandaPal::Organization.settings requires key [:#{previous_keys.push(key).join("][:")}]. It was not found."
|
58
|
+
end
|
59
|
+
if data_type && value
|
60
|
+
if value.class.to_s != data_type
|
61
|
+
record.errors[:settings] << "PandaPal::Organization.settings expected key [:#{previous_keys.push(key).join("][:")}] to be #{data_type} but it was instead #{value.class}."
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
# Verify that anything that is in the real settings has an expectation.
|
67
|
+
if reality
|
68
|
+
if reality.is_a? Hash
|
69
|
+
reality.each do |key, value|
|
70
|
+
was_expected = expectation.has_key?(key)
|
71
|
+
if !was_expected
|
72
|
+
record.errors[:settings] << "PandaPal::Organization.settings had unexpected key: #{key}. If settings have expanded please update your lti_options accordingly."
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
# Recursively check out any children settings as well.
|
78
|
+
if expectation
|
79
|
+
expectation.each do |key, value|
|
80
|
+
validate_level(record, expectation[key], (reality.is_a?(Hash) ? reality.dig(key) : nil), previous_keys.deep_dup.push(key))
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
55
84
|
end
|
56
85
|
end
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
@@ -42,17 +42,6 @@ module PandaPal::Helpers::ControllerHelper
|
|
42
42
|
render plain: 'Invalid Credentials, please contact your Administrator.', :status => :unauthorized unless authorized
|
43
43
|
return authorized
|
44
44
|
end
|
45
|
-
if cookies_need_iframe_fix?
|
46
|
-
fix_iframe_cookies
|
47
|
-
return false
|
48
|
-
end
|
49
|
-
# For safari we may have been launched temporarily full-screen by canvas. This allows us to set the session cookie.
|
50
|
-
# In this case, we should make sure the session cookie is fixed and redirect back to canvas to properly launch the embedded LTI.
|
51
|
-
if params[:platform_redirect_url]
|
52
|
-
session[:safari_cookie_fixed] = true
|
53
|
-
redirect_to params[:platform_redirect_url]
|
54
|
-
return false
|
55
|
-
end
|
56
45
|
return authorized
|
57
46
|
end
|
58
47
|
|
@@ -65,29 +54,8 @@ module PandaPal::Helpers::ControllerHelper
|
|
65
54
|
end
|
66
55
|
end
|
67
56
|
|
68
|
-
# Browsers that prevent 3rd party cookies by default (Safari and IE) run into problems
|
69
|
-
# with CSRF handling because the Rails session cookie isn't set. To fix this, we
|
70
|
-
# redirect the current page to the LTI using JavaScript, which will set the cookie,
|
71
|
-
# and then immediately redirect back to Canvas.
|
72
|
-
def fix_iframe_cookies
|
73
|
-
if params[:safari_cookie_fix].present?
|
74
|
-
session[:safari_cookie_fixed] = true
|
75
|
-
redirect_to params[:return_to]
|
76
|
-
else
|
77
|
-
render 'panda_pal/lti/iframe_cookie_fix', layout: false
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
def cookies_need_iframe_fix?
|
82
|
-
browser.safari? && !request.referrer&.include?('sessionless_launch') && !session[:safari_cookie_fixed] && !params[:platform_redirect_url]
|
83
|
-
end
|
84
|
-
|
85
57
|
def forbid_access_if_lacking_session
|
86
|
-
|
87
|
-
fix_iframe_cookies
|
88
|
-
else
|
89
|
-
render plain: 'You should do an LTI Tool Launch.', status: :unauthorized unless valid_session?
|
90
|
-
end
|
58
|
+
render plain: 'You should do an LTI Tool Launch.', status: :unauthorized unless valid_session?
|
91
59
|
safari_override
|
92
60
|
end
|
93
61
|
|
@@ -114,6 +82,10 @@ module PandaPal::Helpers::ControllerHelper
|
|
114
82
|
end
|
115
83
|
|
116
84
|
def session_key
|
85
|
+
if params[:encrypted_session_key]
|
86
|
+
crypt = ActiveSupport::MessageEncryptor.new(Rails.application.secret_key_base[0..31])
|
87
|
+
return crypt.decrypt_and_verify(params[:encrypted_session_key])
|
88
|
+
end
|
117
89
|
params[:session_key] || session_key_header || flash[:session_key] || session[:session_key]
|
118
90
|
end
|
119
91
|
|
@@ -122,4 +94,37 @@ module PandaPal::Helpers::ControllerHelper
|
|
122
94
|
match[1]
|
123
95
|
end
|
124
96
|
end
|
97
|
+
|
98
|
+
# Redirect with the session key intact. In production,
|
99
|
+
# handle this by encrypting the session key. That way if the
|
100
|
+
# url is logged anywhere, it will all be encrypted data. In dev,
|
101
|
+
# just put it in the URL. Putting it in the URL
|
102
|
+
# is insecure, but is fine in development.
|
103
|
+
# Keeping it in the URL in development means that it plays
|
104
|
+
# nicely with webpack-dev-server live reloading (otherwise
|
105
|
+
# you get an access error everytime it tries to live reload).
|
106
|
+
|
107
|
+
def redirect_with_session_to(location, params = {})
|
108
|
+
if Rails.env.development?
|
109
|
+
redirect_development_mode(location, params)
|
110
|
+
else
|
111
|
+
redirect_production_mode(location, params)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def redirect_development_mode(location, params)
|
116
|
+
redirect_to send(location, {
|
117
|
+
session_key: current_session.session_key,
|
118
|
+
organization_id: current_organization.id
|
119
|
+
}.merge(params))
|
120
|
+
end
|
121
|
+
|
122
|
+
def redirect_production_mode(location, params)
|
123
|
+
crypt = ActiveSupport::MessageEncryptor.new(Rails.application.secret_key_base[0..31])
|
124
|
+
encrypted_data = crypt.encrypt_and_sign(current_session.session_key)
|
125
|
+
redirect_to send(location, {
|
126
|
+
encrypted_session_key: encrypted_data,
|
127
|
+
organization_id: current_organization.id
|
128
|
+
}.merge(params))
|
129
|
+
end
|
125
130
|
end
|
data/lib/panda_pal/version.rb
CHANGED
data/panda_pal.gemspec
CHANGED
@@ -22,9 +22,6 @@ Gem::Specification.new do |s|
|
|
22
22
|
s.add_dependency 'browser', '2.5.0'
|
23
23
|
s.add_dependency 'attr_encrypted', '~> 3.0.0'
|
24
24
|
s.add_dependency 'secure_headers', '~> 6.1.2'
|
25
|
-
|
26
|
-
s.add_development_dependency 'sidekiq'
|
27
|
-
s.add_development_dependency 'sidekiq-scheduler'
|
28
25
|
s.add_development_dependency 'rspec-rails'
|
29
26
|
s.add_development_dependency 'factory_girl_rails'
|
30
27
|
end
|
@@ -1,12 +1,6 @@
|
|
1
1
|
require File.expand_path('../boot', __FILE__)
|
2
2
|
|
3
|
-
require
|
4
|
-
require "active_job/railtie"
|
5
|
-
require "active_record/railtie"
|
6
|
-
# require "active_storage/engine"
|
7
|
-
require "action_controller/railtie"
|
8
|
-
require "action_mailer/railtie"
|
9
|
-
require "action_view/railtie"
|
3
|
+
require 'rails/all'
|
10
4
|
|
11
5
|
Bundler.require(*Rails.groups)
|
12
6
|
require "panda_pal"
|
@@ -22,6 +22,20 @@ Rails.application.configure do
|
|
22
22
|
# Raise an error on page load if there are pending migrations.
|
23
23
|
config.active_record.migration_error = :page_load
|
24
24
|
|
25
|
+
# Debug mode disables concatenation and preprocessing of assets.
|
26
|
+
# This option may cause significant delays in view rendering with a large
|
27
|
+
# number of complex assets.
|
28
|
+
config.assets.debug = true
|
29
|
+
|
30
|
+
# Asset digests allow you to set far-future HTTP expiration dates on all assets,
|
31
|
+
# yet still be able to expire them through the digest params.
|
32
|
+
config.assets.digest = true
|
33
|
+
|
34
|
+
# Adds additional error checking when serving assets at runtime.
|
35
|
+
# Checks for improperly declared sprockets dependencies.
|
36
|
+
# Raises helpful error messages.
|
37
|
+
config.assets.raise_runtime_errors = true
|
38
|
+
|
25
39
|
# Raises error for missing translations
|
26
40
|
# config.action_view.raise_on_missing_translations = true
|
27
41
|
end
|
@@ -24,6 +24,17 @@ Rails.application.configure do
|
|
24
24
|
# Apache or NGINX already handles this.
|
25
25
|
config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present?
|
26
26
|
|
27
|
+
# Compress JavaScripts and CSS.
|
28
|
+
config.assets.js_compressor = :uglifier
|
29
|
+
# config.assets.css_compressor = :sass
|
30
|
+
|
31
|
+
# Do not fallback to assets pipeline if a precompiled asset is missed.
|
32
|
+
config.assets.compile = false
|
33
|
+
|
34
|
+
# Asset digests allow you to set far-future HTTP expiration dates on all assets,
|
35
|
+
# yet still be able to expire them through the digest params.
|
36
|
+
config.assets.digest = true
|
37
|
+
|
27
38
|
# `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
|
28
39
|
|
29
40
|
# Specifies the header that your server uses for sending files.
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# Be sure to restart your server when you modify this file.
|
2
|
+
|
3
|
+
# Version of your assets, change this if you want to expire all your assets.
|
4
|
+
Rails.application.config.assets.version = '1.0'
|
5
|
+
|
6
|
+
# Add additional assets to the asset load path
|
7
|
+
# Rails.application.config.assets.paths << Emoji.images_path
|
8
|
+
|
9
|
+
# Precompile additional assets.
|
10
|
+
# application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
|
11
|
+
# Rails.application.config.assets.precompile += %w( search.js )
|