diff options
-rw-r--r-- | actionpack/lib/action_controller/metal/strong_parameters.rb | 4 | ||||
-rw-r--r-- | actionview/lib/action_view.rb | 1 | ||||
-rw-r--r-- | actionview/lib/action_view/template/resolver.rb | 44 | ||||
-rw-r--r-- | actionview/lib/action_view/testing/resolvers.rb | 4 | ||||
-rw-r--r-- | actionview/lib/action_view/unbound_template.rb | 32 | ||||
-rw-r--r-- | actionview/test/template/file_system_resolver_test.rb | 12 | ||||
-rw-r--r-- | actionview/test/template/optimized_file_system_resolver_test.rb | 12 | ||||
-rw-r--r-- | actionview/test/template/resolver_shared_tests.rb | 146 | ||||
-rw-r--r-- | activerecord/lib/active_record/connection_handling.rb | 27 | ||||
-rw-r--r-- | activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb | 41 | ||||
-rw-r--r-- | guides/source/command_line.md | 16 | ||||
-rw-r--r-- | guides/source/documents.yaml | 5 | ||||
-rw-r--r-- | railties/lib/rails/commands/notes/notes_command.rb | 2 | ||||
-rw-r--r-- | railties/lib/rails/source_annotation_extractor.rb | 15 | ||||
-rw-r--r-- | railties/test/commands/notes_test.rb | 41 |
15 files changed, 365 insertions, 37 deletions
diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index 815f82a1f2..ae774b01f1 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -4,7 +4,6 @@ require "active_support/core_ext/hash/indifferent_access" require "active_support/core_ext/array/wrap" require "active_support/core_ext/string/filters" require "active_support/core_ext/object/to_query" -require "active_support/rescuable" require "action_dispatch/http/upload" require "rack/test" require "stringio" @@ -1092,9 +1091,6 @@ module ActionController # See ActionController::Parameters.require and ActionController::Parameters.permit # for more information. module StrongParameters - extend ActiveSupport::Concern - include ActiveSupport::Rescuable - # Returns a new ActionController::Parameters object that # has been instantiated with the <tt>request.parameters</tt>. def params diff --git a/actionview/lib/action_view.rb b/actionview/lib/action_view.rb index 5ee14bfc78..7f85bf2a5e 100644 --- a/actionview/lib/action_view.rb +++ b/actionview/lib/action_view.rb @@ -44,6 +44,7 @@ module ActionView autoload :Rendering autoload :RoutingUrlFor autoload :Template + autoload :UnboundTemplate autoload :ViewPaths autoload_under "renderer" do diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb index e291dc268a..ce53eb046d 100644 --- a/actionview/lib/action_view/template/resolver.rb +++ b/actionview/lib/action_view/template/resolver.rb @@ -118,7 +118,7 @@ module ActionView locals = locals.map(&:to_s).sort!.freeze cached(key, [name, prefix, partial], details, locals) do - find_templates(name, prefix, partial, details, locals) + _find_all(name, prefix, partial, details, key, locals) end end @@ -131,6 +131,10 @@ module ActionView private + def _find_all(name, prefix, partial, details, key, locals) + find_templates(name, prefix, partial, details, locals) + end + delegate :caching?, to: :class # This is what child classes implement. No defaults are needed @@ -169,35 +173,51 @@ module ActionView else @pattern = DEFAULT_PATTERN end + @unbound_templates = Concurrent::Map.new + super() + end + + def clear_cache + @unbound_templates.clear super() end private - def find_templates(name, prefix, partial, details, locals) + def _find_all(name, prefix, partial, details, key, locals) path = Path.build(name, prefix, partial) - query(path, details, details[:formats], locals) + query(path, details, details[:formats], locals, cache: !!key) end - def query(path, details, formats, locals) + def query(path, details, formats, locals, cache:) template_paths = find_template_paths_from_details(path, details) template_paths = reject_files_external_to_app(template_paths) template_paths.map do |template| - build_template(template, path.virtual, locals) + unbound_template = + if cache + @unbound_templates.compute_if_absent([template, path.virtual]) do + build_unbound_template(template, path.virtual) + end + else + build_unbound_template(template, path.virtual) + end + + unbound_template.bind_locals(locals) end end - def build_template(template, virtual_path, locals) + def build_unbound_template(template, virtual_path) handler, format, variant = extract_handler_and_format_and_variant(template) + source = Template::Sources::File.new(template) - filename = File.expand_path(template) - source = Template::Sources::File.new(filename) - Template.new(source, filename, handler, + UnboundTemplate.new( + source, + template, + handler, virtual_path: virtual_path, format: format, variant: variant, - locals: locals ) end @@ -363,8 +383,8 @@ module ActionView [new(""), new("/")] end - def build_template(template, virtual_path, locals) - super(template, nil, locals) + def build_unbound_template(template, _) + super(template, nil) end def reject_files_external_to_app(files) diff --git a/actionview/lib/action_view/testing/resolvers.rb b/actionview/lib/action_view/testing/resolvers.rb index a97fb71b26..1bedf44934 100644 --- a/actionview/lib/action_view/testing/resolvers.rb +++ b/actionview/lib/action_view/testing/resolvers.rb @@ -23,7 +23,7 @@ module ActionView #:nodoc: private - def query(path, exts, _, locals) + def query(path, exts, _, locals, cache:) query = +"" EXTENSIONS.each do |ext, prefix| query << "(" << exts[ext].map { |e| e && Regexp.escape("#{prefix}#{e}") }.join("|") << "|)" @@ -47,7 +47,7 @@ module ActionView #:nodoc: end class NullResolver < PathResolver - def query(path, exts, _, locals) + def query(path, exts, _, locals, cache:) handler, format, variant = extract_handler_and_format_and_variant(path) [ActionView::Template.new("Template generated by Null Resolver", path.virtual, handler, virtual_path: path.virtual, format: format, variant: variant, locals: locals)] end diff --git a/actionview/lib/action_view/unbound_template.rb b/actionview/lib/action_view/unbound_template.rb new file mode 100644 index 0000000000..db69b6d016 --- /dev/null +++ b/actionview/lib/action_view/unbound_template.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require "concurrent/map" + +module ActionView + class UnboundTemplate + def initialize(source, identifer, handler, options) + @source = source + @identifer = identifer + @handler = handler + @options = options + + @templates = Concurrent::Map.new(initial_capacity: 2) + end + + def bind_locals(locals) + @templates[locals] ||= build_template(locals) + end + + private + + def build_template(locals) + options = @options.merge(locals: locals) + Template.new( + @source, + @identifer, + @handler, + options + ) + end + end +end diff --git a/actionview/test/template/file_system_resolver_test.rb b/actionview/test/template/file_system_resolver_test.rb new file mode 100644 index 0000000000..aa03fdcb13 --- /dev/null +++ b/actionview/test/template/file_system_resolver_test.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "template/resolver_shared_tests" + +class FileSystemResolverTest < ActiveSupport::TestCase + include ResolverSharedTests + + def resolver + ActionView::FileSystemResolver.new(tmpdir) + end +end diff --git a/actionview/test/template/optimized_file_system_resolver_test.rb b/actionview/test/template/optimized_file_system_resolver_test.rb new file mode 100644 index 0000000000..c0c64357ce --- /dev/null +++ b/actionview/test/template/optimized_file_system_resolver_test.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "template/resolver_shared_tests" + +class OptimizedFileSystemResolverTest < ActiveSupport::TestCase + include ResolverSharedTests + + def resolver + ActionView::OptimizedFileSystemResolver.new(tmpdir) + end +end diff --git a/actionview/test/template/resolver_shared_tests.rb b/actionview/test/template/resolver_shared_tests.rb new file mode 100644 index 0000000000..525b452075 --- /dev/null +++ b/actionview/test/template/resolver_shared_tests.rb @@ -0,0 +1,146 @@ +module ResolverSharedTests + attr_reader :tmpdir + + def run(*args) + capture_exceptions do + Dir.mktmpdir(nil, __dir__) { |dir| @tmpdir = dir; super } + end + end + + def with_file(filename, source="File at #{filename}") + path = File.join(tmpdir, filename) + FileUtils.mkdir_p(File.dirname(path)) + File.write(path, source) + end + + def context + @context ||= ActionView::LookupContext.new(resolver) + end + + def test_can_find_with_no_extensions + with_file "test/hello_world", "Hello default!" + + templates = resolver.find_all("hello_world", "test", false, locale: [:en], formats: [:html], variants: [:phone], handlers: [:erb]) + assert_equal 1, templates.size + assert_equal "Hello default!", templates[0].source + assert_equal "test/hello_world", templates[0].virtual_path + assert_nil templates[0].format + assert_nil templates[0].variant + assert_kind_of ActionView::Template::Handlers::Raw, templates[0].handler + end + + def test_can_find_with_just_handler + with_file "test/hello_world.erb", "Hello erb!" + + templates = resolver.find_all("hello_world", "test", false, locale: [:en], formats: [:html], variants: [:phone], handlers: [:erb]) + assert_equal 1, templates.size + assert_equal "Hello erb!", templates[0].source + assert_equal "test/hello_world", templates[0].virtual_path + assert_nil templates[0].format + assert_nil templates[0].variant + assert_kind_of ActionView::Template::Handlers::ERB, templates[0].handler + end + + def test_can_find_with_format_and_handler + with_file "test/hello_world.text.builder", "Hello plain text!" + + templates = resolver.find_all("hello_world", "test", false, locale: [:en], formats: [:html, :text], variants: [:phone], handlers: [:erb, :builder]) + assert_equal 1, templates.size + assert_equal "Hello plain text!", templates[0].source + assert_equal "test/hello_world", templates[0].virtual_path + assert_equal :text, templates[0].format + assert_nil templates[0].variant + assert_kind_of ActionView::Template::Handlers::Builder, templates[0].handler + end + + def test_can_find_with_variant_format_and_handler + with_file "test/hello_world.html+phone.erb", "Hello plain text!" + + templates = resolver.find_all("hello_world", "test", false, locale: [:en], formats: [:html], variants: [:phone], handlers: [:erb]) + assert_equal 1, templates.size + assert_equal "Hello plain text!", templates[0].source + assert_equal "test/hello_world", templates[0].virtual_path + assert_equal :html, templates[0].format + assert_equal "phone", templates[0].variant + assert_kind_of ActionView::Template::Handlers::ERB, templates[0].handler + end + + def test_can_find_with_any_variant_format_and_handler + with_file "test/hello_world.html+phone.erb", "Hello plain text!" + + templates = resolver.find_all("hello_world", "test", false, locale: [:en], formats: [:html], variants: :any, handlers: [:erb]) + assert_equal 1, templates.size + assert_equal "Hello plain text!", templates[0].source + assert_equal "test/hello_world", templates[0].virtual_path + assert_equal :html, templates[0].format + assert_equal "phone", templates[0].variant + assert_kind_of ActionView::Template::Handlers::ERB, templates[0].handler + end + + def test_doesnt_find_template_with_wrong_details + with_file "test/hello_world.html.erb", "Hello plain text!" + + templates = resolver.find_all("hello_world", "test", false, locale: [], formats: [:xml], variants: :any, handlers: [:builder]) + assert_equal 0, templates.size + + templates = resolver.find_all("hello_world", "test", false, locale: [], formats: [:xml], variants: :any, handlers: [:erb]) + assert_equal 0, templates.size + end + + def test_found_template_is_cached + with_file "test/hello_world.html.erb", "Hello HTML!" + + a = context.find("hello_world", "test", false, [], {}) + b = context.find("hello_world", "test", false, [], {}) + assert_same a, b + end + + def test_different_templates_when_cache_disabled + with_file "test/hello_world.html.erb", "Hello HTML!" + + a = context.find("hello_world", "test", false, [], {}) + b = context.disable_cache { context.find("hello_world", "test", false, [], {}) } + c = context.find("hello_world", "test", false, [], {}) + + # disable_cache should give us a new object + refute_same a, b + + # but it should not clear the cache + assert_same a, c + end + + def test_same_template_from_different_details_is_same_object + with_file "test/hello_world.html.erb", "Hello HTML!" + + a = context.find("hello_world", "test", false, [], locale: [:en]) + b = context.find("hello_world", "test", false, [], locale: [:fr]) + assert_same a, b + end + + def test_templates_with_optional_locale_shares_common_object + with_file "test/hello_world.text.erb", "Generic plain text!" + with_file "test/hello_world.fr.text.erb", "Texte en Francais!" + + en = context.find_all("hello_world", "test", false, [], locale: [:en]) + fr = context.find_all("hello_world", "test", false, [], locale: [:fr]) + + assert_equal 1, en.size + assert_equal 2, fr.size + + assert_equal "Generic plain text!", en[0].source + assert_equal "Texte en Francais!", fr[0].source + assert_equal "Generic plain text!", fr[1].source + + assert_same en[0], fr[1] + end + + def test_virtual_path_is_preserved_with_dot + with_file "test/hello_world.html.erb", "Hello html!" + + template = context.find("hello_world.html", "test", false, [], {}) + assert_equal "test/hello_world.html", template.virtual_path + + template = context.find("hello_world", "test", false, [], {}) + assert_equal "test/hello_world", template.virtual_path + end +end diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 53069cd899..6782833c5a 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -85,14 +85,14 @@ module ActiveRecord # based on the requested role: # # ActiveRecord::Base.connected_to(role: :writing) do - # Dog.create! # creates dog using dog connection + # Dog.create! # creates dog using dog writing connection # end # # ActiveRecord::Base.connected_to(role: :reading) do # Dog.create! # throws exception because we're on a replica # end # - # ActiveRecord::Base.connected_to(role: :unknown_ode) do + # ActiveRecord::Base.connected_to(role: :unknown_role) do # # raises exception due to non-existent role # end # @@ -100,11 +100,20 @@ module ActiveRecord # you can use +connected_to+ with a +database+ argument. The +database+ argument # expects a symbol that corresponds to the database key in your config. # - # This will connect to a new database for the queries inside the block. - # # ActiveRecord::Base.connected_to(database: :animals_slow_replica) do # Dog.run_a_long_query # runs a long query while connected to the +animals_slow_replica+ # end + # + # This will connect to a new database for the queries inside the block. By + # default the `:writing` role will be used since all connections must be assigned + # a role. If you would like to use a different role you can pass a hash to database: + # + # ActiveRecord::Base.connected_to(database: { readonly_slow: :animals_slow_replica }) do + # Dog.run_a_long_query # runs a long query while connected to the +animals_slow_replica+ + # using the readonly_slow role. + # end + # + # When using the database key a new connection will be established every time. def connected_to(database: nil, role: nil, &blk) if database && role raise ArgumentError, "connected_to can only accept a `database` or a `role` argument, but not both arguments." @@ -112,17 +121,14 @@ module ActiveRecord if database.is_a?(Hash) role, database = database.first role = role.to_sym - else - role = database.to_sym end config_hash = resolve_config_for_connection(database) handler = lookup_connection_handler(role) - with_handler(role) do - handler.establish_connection(config_hash) - yield - end + handler.establish_connection(config_hash) + + with_handler(role, &blk) elsif role with_handler(role.to_sym, &blk) else @@ -154,6 +160,7 @@ module ActiveRecord end def lookup_connection_handler(handler_key) # :nodoc: + handler_key ||= ActiveRecord::Base.writing_role connection_handlers[handler_key] ||= ActiveRecord::ConnectionAdapters::ConnectionHandler.new end diff --git a/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb index 36591097b6..a2d289bf2f 100644 --- a/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb @@ -203,26 +203,53 @@ module ActiveRecord assert_equal "must provide a `database` or a `role`.", error.message end - def test_switching_connections_with_database_symbol + def test_switching_connections_with_database_symbol_uses_default_role previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" config = { "default_env" => { - "readonly" => { adapter: "sqlite3", database: "db/readonly.sqlite3" }, + "animals" => { adapter: "sqlite3", database: "db/animals.sqlite3" }, "primary" => { adapter: "sqlite3", database: "db/primary.sqlite3" } } } @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config - ActiveRecord::Base.connected_to(database: :readonly) do - assert_equal :readonly, ActiveRecord::Base.current_role - assert ActiveRecord::Base.connected_to?(role: :readonly) + ActiveRecord::Base.connected_to(database: :animals) do + assert_equal :writing, ActiveRecord::Base.current_role + assert ActiveRecord::Base.connected_to?(role: :writing) + + handler = ActiveRecord::Base.connection_handler + assert_equal handler, ActiveRecord::Base.connection_handlers[:writing] + + assert_not_nil pool = handler.retrieve_connection_pool("primary") + assert_equal(config["default_env"]["animals"], pool.spec.config) + end + ensure + ActiveRecord::Base.configurations = @prev_configs + ActiveRecord::Base.establish_connection(:arunit) + ENV["RAILS_ENV"] = previous_env + end + + def test_switching_connections_with_database_hash_uses_passed_role_and_database + previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" + + config = { + "default_env" => { + "animals" => { adapter: "sqlite3", database: "db/animals.sqlite3" }, + "primary" => { adapter: "sqlite3", database: "db/primary.sqlite3" } + } + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + ActiveRecord::Base.connected_to(database: { writing: :primary }) do + assert_equal :writing, ActiveRecord::Base.current_role + assert ActiveRecord::Base.connected_to?(role: :writing) handler = ActiveRecord::Base.connection_handler - assert_equal handler, ActiveRecord::Base.connection_handlers[:readonly] + assert_equal handler, ActiveRecord::Base.connection_handlers[:writing] assert_not_nil pool = handler.retrieve_connection_pool("primary") - assert_equal(config["default_env"]["readonly"], pool.spec.config) + assert_equal(config["default_env"]["primary"], pool.spec.config) end ensure ActiveRecord::Base.configurations = @prev_configs diff --git a/guides/source/command_line.md b/guides/source/command_line.md index a83724f1bb..0091271363 100644 --- a/guides/source/command_line.md +++ b/guides/source/command_line.md @@ -481,6 +481,22 @@ lib/school.rb: * [ 17] [FIXME] ``` +#### Tags + +You can add more default tags to search for by using `config.annotations.register_tags`. It receives a list of tags. + +```ruby +config.annotations.register_tags("DEPRECATEME", "TESTME") +``` + +```bash +$ rails notes +app/controllers/admin/users_controller.rb: + * [ 20] [TODO] do A/B testing on this + * [ 42] [TESTME] this needs more functional tests + * [132] [DEPRECATEME] ensure this method is deprecated in next release +``` + #### Directories You can add more default directories to search from by using `config.annotations.register_directories`. It receives a list of directory names. diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml index 25e4fdb4e6..1e67b2bce7 100644 --- a/guides/source/documents.yaml +++ b/guides/source/documents.yaml @@ -155,6 +155,11 @@ name: Using Rails for API-only Applications url: api_app.html description: This guide explains how to effectively use Rails to develop a JSON API application. + - + name: Active Record and PostgreSQL + work_in_progress: true + url: active_record_postgresql.html + description: This guide covers PostgreSQL specific usage of Active Record. - name: Extending Rails diff --git a/railties/lib/rails/commands/notes/notes_command.rb b/railties/lib/rails/commands/notes/notes_command.rb index 64b339b3cd..94cf183855 100644 --- a/railties/lib/rails/commands/notes/notes_command.rb +++ b/railties/lib/rails/commands/notes/notes_command.rb @@ -5,7 +5,7 @@ require "rails/source_annotation_extractor" module Rails module Command class NotesCommand < Base # :nodoc: - class_option :annotations, aliases: "-a", desc: "Filter by specific annotations, e.g. Foobar TODO", type: :array, default: %w(OPTIMIZE FIXME TODO) + class_option :annotations, aliases: "-a", desc: "Filter by specific annotations, e.g. Foobar TODO", type: :array, default: Rails::SourceAnnotationExtractor::Annotation.tags def perform(*) require_application_and_environment! diff --git a/railties/lib/rails/source_annotation_extractor.rb b/railties/lib/rails/source_annotation_extractor.rb index d7170e6282..9ce22b96a6 100644 --- a/railties/lib/rails/source_annotation_extractor.rb +++ b/railties/lib/rails/source_annotation_extractor.rb @@ -29,6 +29,16 @@ module Rails directories.push(*dirs) end + def self.tags + @@tags ||= %w(OPTIMIZE FIXME TODO) + end + + # Registers additional tags + # Rails::SourceAnnotationExtractor::Annotation.register_tags("TESTME", "DEPRECATEME") + def self.register_tags(*additional_tags) + tags.push(*additional_tags) + end + def self.extensions @@extensions ||= {} end @@ -66,6 +76,8 @@ module Rails # Prints all annotations with tag +tag+ under the root directories +app+, # +config+, +db+, +lib+, and +test+ (recursively). # + # If +tag+ is <tt>nil</tt>, annotations with either default or registered tags are printed. + # # Specific directories can be explicitly set using the <tt>:dirs</tt> key in +options+. # # Rails::SourceAnnotationExtractor.enumerate 'TODO|FIXME', dirs: %w(app lib), tag: true @@ -75,7 +87,8 @@ module Rails # See <tt>#find_in</tt> for a list of file extensions that will be taken into account. # # This class method is the single entry point for the `rails notes` command. - def self.enumerate(tag, options = {}) + def self.enumerate(tag = nil, options = {}) + tag ||= Annotation.tags.join("|") extractor = new(tag) dirs = options.delete(:dirs) || Annotation.directories extractor.display(extractor.find(dirs), options) diff --git a/railties/test/commands/notes_test.rb b/railties/test/commands/notes_test.rb index 147019e299..9182541413 100644 --- a/railties/test/commands/notes_test.rb +++ b/railties/test/commands/notes_test.rb @@ -121,6 +121,47 @@ class Rails::Command::NotesTest < ActiveSupport::TestCase OUTPUT end + test "displays results from additional tags added to the default tags from a config file" do + app_file "app/models/profile.rb", "# TESTME: some method to test" + app_file "app/controllers/hello_controller.rb", "# DEPRECATEME: this action is no longer needed" + app_file "db/some_seeds.rb", "# TODO: default tags such as TODO are still present" + + add_to_config 'config.annotations.register_tags "TESTME", "DEPRECATEME"' + + assert_equal <<~OUTPUT, run_notes_command + app/controllers/hello_controller.rb: + * [1] [DEPRECATEME] this action is no longer needed + + app/models/profile.rb: + * [1] [TESTME] some method to test + + db/some_seeds.rb: + * [1] [TODO] default tags such as TODO are still present + + OUTPUT + end + + test "does not display results from tags that are neither default nor registered" do + app_file "app/models/profile.rb", "# TESTME: some method to test" + app_file "app/controllers/hello_controller.rb", "# DEPRECATEME: this action is no longer needed" + app_file "db/some_seeds.rb", "# TODO: default tags such as TODO are still present" + app_file "db/some_other_seeds.rb", "# BAD: this note should not be listed" + + add_to_config 'config.annotations.register_tags "TESTME", "DEPRECATEME"' + + assert_equal <<~OUTPUT, run_notes_command + app/controllers/hello_controller.rb: + * [1] [DEPRECATEME] this action is no longer needed + + app/models/profile.rb: + * [1] [TESTME] some method to test + + db/some_seeds.rb: + * [1] [TODO] default tags such as TODO are still present + + OUTPUT + end + private def run_notes_command(args = []) rails "notes", args |