zuora_connect-D 1.6.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/Rakefile +38 -0
  4. data/app/assets/javascripts/zuora_connect/api/v1/app_instance.js +2 -0
  5. data/app/assets/javascripts/zuora_connect/application.js +13 -0
  6. data/app/assets/stylesheets/zuora_connect/api/v1/app_instance.css +4 -0
  7. data/app/assets/stylesheets/zuora_connect/application.css +15 -0
  8. data/app/controllers/zuora_connect/admin/tenant_controller.rb +11 -0
  9. data/app/controllers/zuora_connect/api/v1/app_instance_controller.rb +37 -0
  10. data/app/controllers/zuora_connect/application_controller.rb +8 -0
  11. data/app/controllers/zuora_connect/static_controller.rb +26 -0
  12. data/app/helpers/zuora_connect/api/v1/app_instance_helper.rb +4 -0
  13. data/app/helpers/zuora_connect/application_helper.rb +5 -0
  14. data/app/models/zuora_connect/app_instance.rb +5 -0
  15. data/app/models/zuora_connect/app_instance_base.rb +755 -0
  16. data/app/models/zuora_connect/login.rb +37 -0
  17. data/app/views/layouts/zuora_connect/application.html.erb +14 -0
  18. data/app/views/sql/refresh_aggregate_table.txt +84 -0
  19. data/app/views/zuora_connect/static/invalid_app_instance_error.html.erb +65 -0
  20. data/app/views/zuora_connect/static/session_error.html.erb +63 -0
  21. data/config/initializers/apartment.rb +95 -0
  22. data/config/initializers/object_method_hooks.rb +27 -0
  23. data/config/initializers/redis.rb +10 -0
  24. data/config/initializers/resque.rb +5 -0
  25. data/config/initializers/to_bool.rb +24 -0
  26. data/config/routes.rb +12 -0
  27. data/db/migrate/20100718151733_create_connect_app_instances.rb +9 -0
  28. data/db/migrate/20101024162319_add_tokens_to_app_instance.rb +6 -0
  29. data/db/migrate/20101024220705_add_token_to_app_instance.rb +5 -0
  30. data/db/migrate/20110131211919_add_sessions_table.rb +13 -0
  31. data/db/migrate/20110411200303_add_expiration_to_app_instance.rb +5 -0
  32. data/db/migrate/20110413191512_add_new_api_token.rb +5 -0
  33. data/db/migrate/20110503003602_add_catalog_data_to_app_instance.rb +6 -0
  34. data/db/migrate/20110503003603_add_catalog_mappings_to_app_instance.rb +5 -0
  35. data/db/migrate/20110503003604_catalog_default.rb +5 -0
  36. data/db/migrate/20180301052853_add_catalog_attempted_at.rb +5 -0
  37. data/lib/resque/additions.rb +53 -0
  38. data/lib/resque/dynamic_queues.rb +142 -0
  39. data/lib/resque/self_lookup.rb +19 -0
  40. data/lib/tasks/zuora_connect_tasks.rake +24 -0
  41. data/lib/zuora_connect.rb +38 -0
  42. data/lib/zuora_connect/configuration.rb +40 -0
  43. data/lib/zuora_connect/controllers/helpers.rb +165 -0
  44. data/lib/zuora_connect/engine.rb +30 -0
  45. data/lib/zuora_connect/exceptions.rb +67 -0
  46. data/lib/zuora_connect/railtie.rb +35 -0
  47. data/lib/zuora_connect/version.rb +3 -0
  48. data/lib/zuora_connect/views/helpers.rb +9 -0
  49. data/test/controllers/zuora_connect/api/v1/app_instance_controller_test.rb +13 -0
  50. data/test/dummy/README.rdoc +28 -0
  51. data/test/dummy/Rakefile +6 -0
  52. data/test/dummy/app/assets/javascripts/application.js +13 -0
  53. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  54. data/test/dummy/app/controllers/application_controller.rb +5 -0
  55. data/test/dummy/app/helpers/application_helper.rb +2 -0
  56. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  57. data/test/dummy/bin/bundle +3 -0
  58. data/test/dummy/bin/rails +4 -0
  59. data/test/dummy/bin/rake +4 -0
  60. data/test/dummy/bin/setup +29 -0
  61. data/test/dummy/config.ru +4 -0
  62. data/test/dummy/config/application.rb +26 -0
  63. data/test/dummy/config/boot.rb +5 -0
  64. data/test/dummy/config/database.yml +25 -0
  65. data/test/dummy/config/environment.rb +5 -0
  66. data/test/dummy/config/environments/development.rb +41 -0
  67. data/test/dummy/config/environments/production.rb +79 -0
  68. data/test/dummy/config/environments/test.rb +42 -0
  69. data/test/dummy/config/initializers/assets.rb +11 -0
  70. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  71. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  72. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  73. data/test/dummy/config/initializers/inflections.rb +16 -0
  74. data/test/dummy/config/initializers/mime_types.rb +4 -0
  75. data/test/dummy/config/initializers/session_store.rb +3 -0
  76. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  77. data/test/dummy/config/locales/en.yml +23 -0
  78. data/test/dummy/config/routes.rb +4 -0
  79. data/test/dummy/config/secrets.yml +22 -0
  80. data/test/dummy/db/development.sqlite3 +0 -0
  81. data/test/dummy/db/test.sqlite3 +0 -0
  82. data/test/dummy/log/development.log +2 -0
  83. data/test/dummy/log/test.log +0 -0
  84. data/test/dummy/public/404.html +67 -0
  85. data/test/dummy/public/422.html +67 -0
  86. data/test/dummy/public/500.html +66 -0
  87. data/test/dummy/public/favicon.ico +0 -0
  88. data/test/fixtures/zuora_connect/app_instances.yml +11 -0
  89. data/test/integration/navigation_test.rb +8 -0
  90. data/test/lib/generators/zuora_connect/datatable_generator_test.rb +16 -0
  91. data/test/models/zuora_connect/app_instance_test.rb +9 -0
  92. data/test/test_helper.rb +21 -0
  93. data/test/zuora_connect_test.rb +7 -0
  94. metadata +408 -0
@@ -0,0 +1,37 @@
1
+ module ZuoraConnect
2
+ class Login
3
+
4
+ def initialize (fields)
5
+ @clients = {}
6
+ if fields["tenant_type"] == "Zuora"
7
+ login_fields = fields.map{|k,v| [k.to_sym, v]}.to_h
8
+
9
+ fields["authentication_type"] = "basic" if fields["authentication_type"].nil? || fields["authentication_type"].blank?
10
+
11
+ @clients["Default"] = "::ZuoraAPI::#{fields["authentication_type"].capitalize}".constantize.new(login_fields)
12
+ @default_entity = fields["entities"][0]["id"] if fields["entities"].size == 1 if fields["entities"] && fields["entities"].size > 0
13
+ if fields["entities"] && fields["entities"].size > 0
14
+ fields["entities"].each do |entity|
15
+ params = {:entity_id => entity["id"]}.merge(login_fields)
16
+ @clients[entity["id"]] = "::ZuoraAPI::#{fields["authentication_type"].capitalize}".constantize.new(params)
17
+ end
18
+ end
19
+ self.attr_builder("available_entities", @clients.keys)
20
+ end
21
+ fields.each do |k,v|
22
+ self.attr_builder(k,v)
23
+ end
24
+ @default_entity ||= "Default"
25
+ end
26
+
27
+ def attr_builder(field,val)
28
+ singleton_class.class_eval { attr_accessor "#{field}" }
29
+ send("#{field}=", val)
30
+ end
31
+
32
+ def client(id = @default_entity)
33
+ return id.blank? ? @clients[@default_entity] : @clients[id]
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Connect</title>
5
+ <%= stylesheet_link_tag "zuora_connect/application", media: "all" %>
6
+ <%= javascript_include_tag "zuora_connect/application" %>
7
+ <%= csrf_meta_tags %>
8
+ </head>
9
+ <body>
10
+
11
+ <%= yield %>
12
+
13
+ </body>
14
+ </html>
@@ -0,0 +1,84 @@
1
+ CREATE OR REPLACE FUNCTION "shared_extensions".refresh_aggregate_table(aggregate_table_name text, table_name text, filter text, mode text) RETURNS void AS $$
2
+ DECLARE
3
+ schema RECORD;
4
+ result RECORD;
5
+ sql TEXT := '';
6
+ i INTEGER;
7
+ created boolean := false;
8
+ fields_order character varying;
9
+ index_name varchar;
10
+ index_string varchar;
11
+ index_id varchar;
12
+ BEGIN
13
+ IF mode = 'Table' THEN
14
+ raise notice 'Starting aggregate of % to %', table_name, aggregate_table_name;
15
+
16
+
17
+ EXECUTE format('DROP TABLE IF EXISTS "public".%I', aggregate_table_name);
18
+ raise notice 'Filter %', filter;
19
+
20
+ FOR schema IN EXECUTE
21
+ format(
22
+ 'SELECT schema_name FROM information_schema.schemata WHERE schema_name ~ ''^[0-9]+$'''
23
+ )
24
+ LOOP
25
+ IF NOT created THEN
26
+ -- Create the aggregate table if we haven't already
27
+ EXECUTE format(
28
+ 'CREATE TABLE "public".%I (LIKE %I.%I)',
29
+ aggregate_table_name,
30
+ schema.schema_name, table_name
31
+ );
32
+ -- Add a special `schema_name` column, which we'll populate with the name of the schema
33
+ -- each row originated from
34
+ EXECUTE format(
35
+ 'ALTER TABLE "public".%I ADD COLUMN schema_name text', aggregate_table_name
36
+ );
37
+ created := true;
38
+ END IF;
39
+
40
+ -- Finally, we'll select everything from this schema's target table, plus the schema's name,
41
+ -- and insert them into our new aggregate table
42
+ EXECUTE format(
43
+ 'SELECT string_agg(column_name, '','') from information_schema.columns where table_name = ''%s'' AND table_schema = ''%s''',
44
+ table_name, schema.schema_name
45
+ ) into fields_order;
46
+
47
+ raise notice 'Importing Schema %', schema.schema_name;
48
+
49
+ EXECUTE format(
50
+ 'INSERT INTO "public".%I (schema_name, %s) (SELECT ''%s'' AS schema_name, * FROM %I.%I %s )',
51
+ aggregate_table_name,
52
+ fields_order,
53
+ schema.schema_name,
54
+ schema.schema_name, table_name,
55
+ filter
56
+ );
57
+ END LOOP;
58
+
59
+ EXECUTE
60
+ format('CREATE INDEX ON "public".%I (schema_name)', aggregate_table_name);
61
+ EXECUTE
62
+ format('CREATE INDEX ON "public".%I (id)', aggregate_table_name);
63
+ END IF;
64
+ IF mode = 'Index' THEN
65
+ FOR index_string, index_name, index_id IN
66
+ SELECT pg_get_indexdef(idx.oid)||';', idx.relname, idx.oid
67
+ from pg_index ind
68
+ join pg_class idx on idx.oid = ind.indexrelid
69
+ join pg_class tbl on tbl.oid = ind.indrelid
70
+ left join pg_namespace ns on ns.oid = tbl.relnamespace where idx.relname != concat(table_name, '_pkey') and tbl.relname = table_name and ns.nspname = 'public'
71
+ LOOP
72
+ BEGIN
73
+ EXECUTE
74
+ format('DROP INDEX IF EXISTS "public"."%s"', concat(aggregate_table_name, '_', index_id));
75
+
76
+ EXECUTE
77
+ format(replace(replace(index_string, index_name, concat(aggregate_table_name, '_', index_id)), concat(' ', table_name, ' '), concat( ' ', aggregate_table_name, ' ')));
78
+
79
+ RAISE NOTICE 'Creating Indexes %', replace(replace(replace(index_string, index_name, concat(aggregate_table_name, '_', index_id)), concat(' ', table_name, ' '), concat( ' ', aggregate_table_name, ' ')), concat('public.', table_name), concat( 'public.', aggregate_table_name)) ;
80
+ END;
81
+ END LOOP;
82
+ END IF;
83
+ END
84
+ $$ LANGUAGE plpgsql;
@@ -0,0 +1,65 @@
1
+ <html><head>
2
+ <title>We're sorry, but something went wrong (500)</title>
3
+ <meta name="viewport" content="width=device-width,initial-scale=1">
4
+ <style>
5
+ body {
6
+ background-color: #EFEFEF;
7
+ color: #2E2F30;
8
+ text-align: center;
9
+ font-family: arial, sans-serif;
10
+ margin: 0;
11
+ }
12
+
13
+ div.dialog {
14
+ width: 95%;
15
+ max-width: 33em;
16
+ margin: 4em auto 0;
17
+ }
18
+
19
+ div.dialog > div {
20
+ border: 1px solid #CCC;
21
+ border-right-color: #999;
22
+ border-left-color: #999;
23
+ border-bottom-color: #BBB;
24
+ border-top: #B00100 solid 4px;
25
+ border-top-left-radius: 9px;
26
+ border-top-right-radius: 9px;
27
+ background-color: white;
28
+ padding: 7px 12% 0;
29
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
30
+ }
31
+
32
+ h1 {
33
+ font-size: 100%;
34
+ color: #730E15;
35
+ line-height: 1.5em;
36
+ }
37
+
38
+ div.dialog > p {
39
+ margin: 0 0 1em;
40
+ padding: 1em;
41
+ background-color: #F7F7F7;
42
+ border: 1px solid #CCC;
43
+ border-right-color: #999;
44
+ border-left-color: #999;
45
+ border-bottom-color: #999;
46
+ border-bottom-left-radius: 4px;
47
+ border-bottom-right-radius: 4px;
48
+ border-top-color: #DADADA;
49
+ color: #666;
50
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
51
+ }
52
+ </style>
53
+ </head>
54
+
55
+ <body>
56
+ <!-- This file lives in public/500.html -->
57
+ <div class="dialog">
58
+ <div>
59
+ <h1>We're sorry, but this request could not be verified.</h1>
60
+ </div>
61
+ <p>Please try relaunching this application at connect.zuora.com</p>
62
+ </div>
63
+
64
+
65
+ </body></html>
@@ -0,0 +1,63 @@
1
+ <html><head>
2
+ <title>We're sorry, but something went wrong (500)</title>
3
+ <meta name="viewport" content="width=device-width,initial-scale=1">
4
+ <style>
5
+ body {
6
+ background-color: #EFEFEF;
7
+ color: #2E2F30;
8
+ text-align: center;
9
+ font-family: arial, sans-serif;
10
+ margin: 0;
11
+ }
12
+
13
+ div.dialog {
14
+ width: 95%;
15
+ max-width: 33em;
16
+ margin: 4em auto 0;
17
+ }
18
+
19
+ div.dialog > div {
20
+ border: 1px solid #CCC;
21
+ border-right-color: #999;
22
+ border-left-color: #999;
23
+ border-bottom-color: #BBB;
24
+ border-top: #B00100 solid 4px;
25
+ border-top-left-radius: 9px;
26
+ border-top-right-radius: 9px;
27
+ background-color: white;
28
+ padding: 7px 12% 0;
29
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
30
+ }
31
+
32
+ h1 {
33
+ font-size: 100%;
34
+ color: #730E15;
35
+ line-height: 1.5em;
36
+ }
37
+
38
+ div.dialog > p {
39
+ margin: 0 0 1em;
40
+ padding: 1em;
41
+ background-color: #F7F7F7;
42
+ border: 1px solid #CCC;
43
+ border-right-color: #999;
44
+ border-left-color: #999;
45
+ border-bottom-color: #999;
46
+ border-bottom-left-radius: 4px;
47
+ border-bottom-right-radius: 4px;
48
+ border-top-color: #DADADA;
49
+ color: #666;
50
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
51
+ }
52
+ </style>
53
+ </head>
54
+
55
+ <body>
56
+ <!-- This file lives in public/500.html -->
57
+ <div class="dialog">
58
+ <div>
59
+ <h1>Session is invalid</h1>
60
+ </div>
61
+ <p>Please try relaunching the application from connect.zuora.com</p>
62
+ </div>
63
+ </body></html>
@@ -0,0 +1,95 @@
1
+ # You can have Apartment route to the appropriate Tenant by adding some Rack middleware.
2
+ # Apartment can support many different "Elevators" that can take care of this routing to your data.
3
+ # Require whichever Elevator you're using below or none if you have a custom one.
4
+ #
5
+ # require 'apartment/elevators/generic'
6
+ # require 'apartment/elevators/domain'
7
+ #require 'apartment/elevators/subdomain'
8
+ # require 'apartment/elevators/first_subdomain'
9
+ #
10
+ # Apartment Configuration
11
+ #
12
+ Apartment.configure do |config|
13
+
14
+ # Add any models that you do not want to be multi-tenanted, but remain in the global (public) namespace.
15
+ # A typical example would be a Customer or Tenant model that stores each Tenant's information.
16
+ #
17
+ # config.excluded_models = %w{ Tenant }
18
+
19
+ # In order to migrate all of your Tenants you need to provide a list of Tenant names to Apartment.
20
+ # You can make this dynamic by providing a Proc object to be called on migrations.
21
+ # This object should yield either:
22
+ # - an array of strings representing each Tenant name.
23
+ # - a hash which keys are tenant names, and values custom db config (must contain all key/values required in database.yml)
24
+ #
25
+ # config.tenant_names = lambda{ Customer.pluck(:tenant_name) }
26
+ # config.tenant_names = ['tenant1', 'tenant2']
27
+ # config.tenant_names = {
28
+ # 'tenant1' => {
29
+ # adapter: 'postgresql',
30
+ # host: 'some_server',
31
+ # port: 5555,
32
+ # database: 'postgres' # this is not the name of the tenant's db
33
+ # # but the name of the database to connect to before creating the tenant's db
34
+ # # mandatory in postgresql
35
+ # },
36
+ # 'tenant2' => {
37
+ # adapter: 'postgresql',
38
+ # database: 'postgres' # this is not the name of the tenant's db
39
+ # # but the name of the database to connect to before creating the tenant's db
40
+ # # mandatory in postgresql
41
+ # }
42
+ # }
43
+ # config.tenant_names = lambda do
44
+ # Tenant.all.each_with_object({}) do |tenant, hash|
45
+ # hash[tenant.name] = tenant.db_configuration
46
+ # end
47
+ # end
48
+ #
49
+ config.tenant_names = lambda { ZuoraConnect::AppInstance.pluck :id }
50
+ if defined?(ActiveRecord::SessionStore::Session)
51
+ config.excluded_models = ["ZuoraConnect::AppInstance","ZuoraConnect::AppInstanceBase", "ActiveRecord::SessionStore::Session"]
52
+ else
53
+ config.excluded_models = ["ZuoraConnect::AppInstance","ZuoraConnect::AppInstanceBase"]
54
+ end
55
+ #
56
+ # ==> PostgreSQL only options
57
+
58
+ # Specifies whether to use PostgreSQL schemas or create a new database per Tenant.
59
+ # The default behaviour is true.
60
+ #
61
+ config.use_schemas = true
62
+
63
+ # Apartment can be forced to use raw SQL dumps instead of schema.rb for creating new schemas.
64
+ # Use this when you are using some extra features in PostgreSQL that can't be respresented in
65
+ # schema.rb, like materialized views etc. (only applies with use_schemas set to true).
66
+ # (Note: this option doesn't use db/structure.sql, it creates SQL dump by executing pg_dump)
67
+ #
68
+ # config.use_sql = false
69
+
70
+ # There are cases where you might want some schemas to always be in your search_path
71
+ # e.g when using a PostgreSQL extension like hstore.
72
+ # Any schemas added here will be available along with your selected Tenant.
73
+ #
74
+ config.persistent_schemas = %w{ shared_extensions }
75
+
76
+ # <== PostgreSQL only options
77
+ #
78
+
79
+ # By default, and only when not using PostgreSQL schemas, Apartment will prepend the environment
80
+ # to the tenant name to ensure there is no conflict between your environments.
81
+ # This is mainly for the benefit of your development and test environments.
82
+ # Uncomment the line below if you want to disable this behaviour in production.
83
+ #
84
+ # config.prepend_environment = !Rails.env.production?
85
+ end
86
+
87
+ # Setup a custom Tenant switching middleware. The Proc should return the name of the Tenant that
88
+ # you want to switch to.
89
+ # Rails.application.config.middleware.use 'Apartment::Elevators::Generic', lambda { |request|
90
+ # request.host.split('.').first
91
+ # }
92
+
93
+ # Rails.application.config.middleware.use 'Apartment::Elevators::Domain'
94
+ #Rails.application.config.middleware.use 'Apartment::Elevators::Subdomain'
95
+ # Rails.application.config.middleware.use 'Apartment::Elevators::FirstSubdomain'
@@ -0,0 +1,27 @@
1
+ class Object
2
+ def self.method_hook(*args)
3
+ options = args.extract_options!
4
+ return unless (options[:before].present? or options[:after].present?)
5
+ args.each do |method_name|
6
+ old_method = instance_method(method_name) rescue next
7
+
8
+ define_method(method_name) do |*args|
9
+ # invoke before callback
10
+ if options[:before].present?
11
+ options[:before].is_a?(Proc) ? options[:before].call(method_name, self):
12
+ send(options[:before], method_name)
13
+ end
14
+
15
+ # you can modify the code to call after callback
16
+ # only when the old method returns true etc..
17
+ old_method.bind(self).call(*args)
18
+
19
+ # invoke after callback
20
+ if options[:after].present?
21
+ options[:after].is_a?(Proc) ? options[:after].call(method_name, self):
22
+ send(options[:after], method_name)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,10 @@
1
+ redis_url = ENV["REDIS_URL"].present? ? ENV["REDIS_URL"] : defined?(Rails.application.secrets.redis) ? Rails.application.secrets.redis : 'redis://localhost:6379/1'
2
+ if defined?(Redis.current)
3
+ Redis.current = Redis.new(:url => redis_url, :timeout => 10, :reconnect_attempts => 2)
4
+ if defined?(Resque.redis)
5
+ Resque.redis = Redis.current
6
+ end
7
+ end
8
+ if defined?(RedisBrowser)
9
+ RedisBrowser.configure("connections" => { "default" => { "url" => redis_url } })
10
+ end
@@ -0,0 +1,5 @@
1
+ if defined?(Resque::Worker)
2
+ Resque.send(:extend, Resque::Additions)
3
+ Resque::Worker.send(:include, Resque::DynamicQueues)
4
+ Resque::Job.send(:include, Resque::SelfLookup)
5
+ end
@@ -0,0 +1,24 @@
1
+ class String
2
+ def to_bool
3
+ return self if (self.class == TrueClass || self.class == FalseClass)
4
+ return true if self == true || self =~ (/(true|t|yes|y|1)$/i)
5
+ return false if self == false || self.blank? || self =~ (/(false|f|no|n|0)$/i)
6
+ return false
7
+ raise ArgumentError.new("invalid value for Boolean: \"#{self}\"")
8
+ end
9
+ end
10
+ class TrueClass
11
+ def to_bool
12
+ return self
13
+ end
14
+ end
15
+ class FalseClass
16
+ def to_bool
17
+ return self
18
+ end
19
+ end
20
+ class NilClass
21
+ def to_bool
22
+ return false
23
+ end
24
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,12 @@
1
+ ZuoraConnect::Engine.routes.draw do
2
+ get '/health' => 'static#health'
3
+ get '/invalid_session' => 'static#session_error', :as => :invalid_session
4
+ get '/invalid_instance' => "static#invalid_app_instance_error", :as => :invalid_instance
5
+ namespace :api do
6
+ namespace :v1 do
7
+ resources :app_instance, :only => [:index], defaults: {format: :json} do
8
+ match "drop", via: [:get, :post], on: :collection
9
+ end
10
+ end
11
+ end
12
+ end