Rails 3 - default_url_options based on url being generated - ruby-on-rails

In rails 3, is it possible to gain access to the controller/action of the URL being generated inside of default_url_options()? In rails 2 you were passed a Hash of the options that were about to be passed to url_for() that you could of course alter.
E.g. Rails 2 code:
==== config/routes.rb
map.foo '/foo/:shard/:id', :controller => 'foo', :action => 'show'
==== app/controllers/application.rb
def default_url_options options = {}
options = super(options)
if options[:controller] == 'some_controller' and options[:id]
options[:shard] = options[:id].to_s[0..2]
end
options
end
==== anywhere
foo_path(:id => 12345) # => /foo/12/12345
However, in rails 3, that same code fails due to the fact that default_url_options is not passed any options hash, and I have yet to find out how to test what the controller is.
FWIW, the above "sharding" is due to when you turn caching on, if you have a large number of foo rows in your DB, then you're going to hit the inode limit on unix based systems for number of files in 1 folder at some point. The correct "fix" here is to probably alter the cache settings to store the file in the sharded path rather than shard the route completely. At the time of writing the above code though, part of me felt it was nice to always have the cached file in the same structure as the route, in case you ever wanted something outside of rails to serve the cache.
But alas, I'd still be interested in a solution for the above, purely because it's eating at me that I couldn't figure it out.
Edit: Currently I have the following which I'll have to ditch, since you lose all other named_route functionality.
==== config/routes.rb
match 'foo/:shard/:id' => 'foo#show', :as => 'original_foo'
==== app/controllers/application.rb
helpers :foo_path
def foo_path *args
opts = args.first if opts.is_a?(Array)
args = opts.merge(:shard => opts[:id].to_s[0..2]) if opts.is_a?(Hash) and opts[:id]
original_foo_path(args)
end

define a helper like
# app/helpers/foo_helper.rb
module FooHelper
def link_to name, options = {}, &block
options[:shard] = options[:id].to_s[0..1] if options[:id]
super name, options, &block
end
end
and then do the following in your view, seems to work for me
<%= link_to("my shard", id: 12345) %>
edit: or customize the foo_path as
module FooHelper
def link_to name, options = {}, &block
options[:shard] = options[:id].to_s[0..1] if options[:id]
super name, options, &block
end
def foo_path options = {}
options[:shard] = options[:id].to_s[0..1] if options[:id]
super options
end
end

Related

How to track objects "called" inside a block?

Question:
I need to know the records' attributes that have been called inside a block (say I need something like the following):
def my_custom_method(&block)
some_method_that_starts_tracking
block.call
some_method_that_stops_tracking
puts some_method_that_returns_called_records_attributes
do_something_about(some_method_that_returns_called_records_attributes)
end
my_custom_method { somecodethatcallsauthorofbook1andemailandfirstnameofuser43 }
# this is the `puts` output above (just as an example)
# => {
# #<Book id:1...> => [:author],
# #<User id:43...> => [:email, :first_name]
# }
code inside the block can be anything
Specifically, I meant to track any instance of a subclass of ApplicationRecord, so it can be instance of any models like Book, User, etc...
Attempts:
From my understanding, this is similar to how rspec works when a method is expected to be called. That it somehow tracks any calls of that method. So, my initial attempt is to do something like the following (which does not yet fully work):
def my_custom_method(&block)
called_records_attributes = {}
ApplicationRecord.descendants.each do |klass|
klass.class_eval do
attribute_names.each do |attribute_name|
define_method(attribute_name) do
called_records_attributes[self] ||= []
called_records_attributes[self] << attribute_name
self[attribute_name]
end
end
end
end
block.call
# the above code will work but at this point, I don't know how to clean the methods that were defined above, as the above define_methods should only be temporary
puts called_records_attributes
end
my_custom_method { Book.find_by(id: 1).title }
# => {
# #<Book id: 1...> => ['title']
# }
the .descendants above probably is not a good idea because Rails use autoload if I'm not mistaken
as already said above in the comment, I do not know how to remove these "defined_methods" that are just supposed to be only temporary for the duration of this "block".
furthermore, my code above would probably have overriden the "actual" attribute getters of the models, if ever any has been already defined, which is bad.
Background:
I am writing a gem live_record which I am adding a new feature that will allow a developer to just simply write something like
<!-- app/views/application.html.erb -->
<body>
<%= live_record_sync { #book.some_custom_method_about_book } %>
</body>
... which will render #book.some_custom_method_about_book as-is on the page, but at the same time the live_record_sync wrapper method would take note of all the attributes that have been called inside that block (i.e. inside some_custom_method_about_book the #book.title is called), and then it sets these attributes as the block's own "dependencies", in which later when that specific book's attribute has been updated, I can already also update directly the HTML page of which this attribute is a "dependency" as like specified just above. I am aware that this is not an accurate solution, but I'd like to open up my chances by experimenting on this first.
-- Rails 5
Disclaimer: I believe this is just a mediocre solution, but hopefully helps anyone with the same problem.
I tried reading rspec source code, but because I couldn't easily comprehend what is happening under the hood, and that it occurred to me that rspec's (i.e.) expect(Book.first).to receive(:title) is different from what I really want because the methods there are already specified (i.e. :title), while what I want is to track ANY methods that are attributes, so because of these two reasons I skipped reading further, and attempted my own solution, which hopefully did somehow work; see below.
Note that I am using Thread local-storage here, so this code should be thread-safe (untested yet).
# lib/my_tracker.rb
class MyTracker
Thread.current[:my_tracker_current_tracked_records] = {}
attr_accessor :tracked_records
class << self
def add_to_tracked_records(record, attribute_name)
Thread.current[:my_tracker_current_tracked_records][{model: record.class.name.to_sym, record_id: record.id}] ||= []
Thread.current[:my_tracker_current_tracked_records][{model: record.class.name.to_sym, record_id: record.id}] << attribute_name
end
end
def initialize(block)
#block = block
end
def call_block_while_tracking_records
start_tracking
#block_evaluated_value = #block.call
#tracked_records = Thread.current[:my_tracker_current_tracked_records]
stop_tracking
end
def to_s
#block_evaluated_value
end
# because I am tracking record-attributes, and you might want to track a different object / method, then you'll need to write your own `prepend` extension (look for how to use `prepend` in ruby)
module ActiveRecordExtensions
def _read_attribute(attribute_name)
if Thread.current[:my_tracker_current_tracked_records] && !Thread.current[:my_tracker_is_tracking_locked] && self.class < ApplicationRecord
# I added this "lock" to prevent infinite loop inside `add_to_tracked_records` as I am calling the record.id there, which is then calling this _read_attribute, and then loops.
Thread.current[:my_tracker_is_tracking_locked] = true
::MyTracker.add_to_tracked_records(self, attribute_name)
Thread.current[:my_tracker_is_tracking_locked] = false
end
super(attribute_name)
end
end
module Helpers
def track_records(&block)
my_tracker = MyTracker.new(block)
my_tracker.call_block_while_tracking_records
my_tracker
end
end
private
def start_tracking
Thread.current[:my_tracker_current_tracked_records] = {}
end
def stop_tracking
Thread.current[:my_tracker_current_tracked_records] = nil
end
end
ActiveSupport.on_load(:active_record) do
prepend MyTracker::ActiveRecordExtensions
end
ActiveSupport.on_load(:action_view) do
include MyTracker::Helpers
end
ActiveSupport.on_load(:action_controller) do
include MyTracker::Helpers
end
Usage Example
some_controller.rb
book = Book.find_by(id: 1)
user = User.find_by(id: 43)
my_tracker = track_records do
book.title
if user.created_at == book.created_at
puts 'same date'
end
'thisisthelastlineofthisblockandthereforewillbereturned'
end
puts my_tracker.class
# => #<MyTracker ... >
puts my_tracker.tracked_records
# => {
# {model: :Book, record_id: 1} => ['title', 'created_at'],
# {model: :User, record_id: 43} => ['created_at']
# }
puts my_tracker
# => 'thisisthelastlineofthisblockandthereforewillbereturned'
# notice that `puts my_tracker` above prints out the block itself
# this is because I defined `.to_s` above.
# I need this `.to_s` so I can immediately print the block as-is in the views.
# see example below
some_view.html.erb
<%= track_records { current_user.email } %>
P.S. Maybe it's better that I wrap this up as a gem. If you're interested, let me know

Rails/Ruby incorrectly showing variable not defined

In debugging console, while app running (using binding.pry to interrupt it), I can see that my variable Rails.configuration.hardcoded_current_user_key is set:
pry(#<TasksController>)> Rails.configuration.hardcoded_current_user_key
=> "dev"
But it doesn't appear to be defined:
pry(#<TasksController>)> defined?(Rails.configuration.hardcoded_current_user_key)
=> nil
Yet it works fine to store and test its value:
pry(#<TasksController>)> tempVar = Rails.configuration.hardcoded_current_user_key
=> "dev"
pry(#<TasksController>)> defined?(tempVar)
=> "local-variable"
What is going on?
This is because Rails config implements respond_to? but not respond_to_missing?, and defined? only recognizes respond_to_missing?:
class X
def respond_to?(name, include_all = false)
name == :another_secret || super
end
private
def method_missing(name, *args, &block)
case name
when :super_secret
'Bingo!'
when :another_secret
'Nope.'
else
super
end
end
def respond_to_missing?(name, include_all = false)
name == :super_secret || super
end
end
x = X.new
puts x.super_secret # => Bingo!
p defined?(x.super_secret) # => "method"
puts x.another_secret # => Nope.
p defined?(x.another_secret) # => nil
It's recommended to implement respond_to_missing? along with method_missing, I too wonder why Rails did it that way.
You shouldn't be using defined? on anything but the "stub" of that, or in other words, merely this:
defined?(Rails)
Anything beyond that is highly unusual to see, and I'm not even sure it's valid.
defined? is not a method, but a construct that tests if the following thing is defined as a variable, constant or method, among other things. It won't evaluate your code, it will just test it as-is. This means method calls don't happen, and as such, can't be chained.
If you want to test that something is assigned, then you should use this:
Rails.configuration.hardcoded_current_user_key.nil?

content_for blank when logged in

I'm implementing (more or less) WordPress's concept of shortcodes in my Rails app. The problem is that when I :yield anything in my layout defined via content_for in the view, it's just blank. So the additional javascript and title tags are not rendered.
In other words, calling content_for? :title in the layout returns false.
This only happens in posts/index, and only when I'm logged in when the filter_shortcodes helper is run. Has anyone encountered something like this before?
In views/posts/index.html.haml:
- content_for :script do
= javascript_include_tag '/assets/autoload.js'
- content_for :title do
Blog
...
= render template: 'article-content',
And in views/article-content.html.haml (filter_shortcodes is a helper function defined in the Shortcode module.):
:plain
#{filter_shortcodes instance.content}
I remain convinced that the problem is in my shortcode module, so here it is, back by unpopular demand:
module Shortcode
def filter_shortcodes content
content.gsub /(?<!\\)\[.+\]/ do |code|
# A shortcode must:
# - be on its own line
# - be [contained within square brackets]
# - be named using only lowercase letters
# If it contains parameters, they must come in the form:
# key="value"
shortcode = /^\s*\[(?<name>[a-z]+) (?<params>.*)\s*\]\s*$/.match code
params_list = shortcode[:params].gsub /"|"/, '"'
param_regexp = /([a-z]+)="([^"]*)"/
shortcode_params = {}
params_list.scan param_regexp do |param|
shortcode_params[param[0].to_sym] = param[1]
end
render_to_string template: "shortcodes/#{shortcode[:name]}",
:locals => shortcode_params, layout: false
end
end
end
Turns out it was a problem with the shortcode module. Basically, render_to_string behaves like render, and you can only make one call to either in a controller action. So, because filter_shortcode was behaving as a controller method (through the module's inclusion in my controller class), render_to_string violated the once-only principal.
It'd be nice if Rails enforced this the way they do when you try to call render twice, but maybe this is an edge case?
Anyway, my solution was to render a cell instead of a template. The below code is a little more verbose, but it's also more robust because it allows you to do shortcode-specific validation:
lib/shortcode.rb:
module Shortcode
def filter_shortcodes content
# A shortcode must:
# - be on its own line
# - be [contained within square brackets]
# - be named using only lowercase letters
# - be unescaped (not preceded by a "\")
# If it contains parameters, they must come in the form:
# key="value"
regex = /^\s*\[(?<name>[\w]+)\s+(?<params>([^\]]+\s*)*)?\](?<contents>([^\[])*)?(\[(\\|\/)[a-z]+\])?$/
# Here's the negative lookbehind for the escaping \
content.gsub /(?<!\\)\[.+\]/ do |code|
shortcode = regex.match code
# return shortcode[:params]
params = get_params shortcode
params[:contents] = shortcode[:contents] if shortcode[:contents].present?
if ShortcodeCell.method_defined? shortcode[:name]
# just spit out the content if the template doesn't exist
begin
render_cell :shortcode, shortcode[:name], params
rescue ArgumentError => error
"[#{shortcode[:name]} ERROR: #{error.message}]"
end
else
"Unsupported shortcode: #{shortcode[:name]}"
end
end
end
def get_params shortcode
# hack to render quotes...
params_list = shortcode[:params].gsub /"|"/, '"'
params = {}
params_list.scan /([a-z]+)="([^"]*)"/ do |param|
params[param[0].to_sym] = param[1]
end
return params
end
cells/shortcode_cell.rb:
class ShortcodeCell < Cell::Rails
def soundcloud args
if args[:url].blank?
raise ArgumentError.new 'missing URL'
end
render locals: args
end
end

Capybara + remote form request

I have a form that I'm testing using Capybara. This form's URL goes to my Braintree sandbox, although I suspect the problem would happen for any remote URL. When Capybara clicks the submit button for the form, the request is routed to the dummy application rather than the remote service.
Here's an example app that reproduces this issue: https://github.com/radar/capybara_remote. Run bundle exec ruby test/form_test.rb and the test will pass, which is not what I'd typically expect.
Why does this happen and is this behaviour that I can rely on always happening?
Mario Visic points out this description in the Capybara documentation:
Furthermore, you cannot use the RackTest driver to test a remote application, or to access remote URLs (e.g., redirects to external sites, external APIs, or OAuth services) that your application might interact with.
But I wanted to know why, so I source dived. Here's my findings:
lib/capybara/node/actions.rb
def click_button(locator)
find(:button, locator).click
end
I don't care about the find here because that's working. It's the click that's more interesting. That method is defined like this:
lib/capybara/node/element.rb
def click
wait_until { base.click }
end
I don't know what base is, but I see the method is defined twice more in lib/capybara/rack_test/node.rb and lib/capybara/selenium/node.rb. The tests are using Rack::Test and not Selenium, so it's probably the former:
lib/capybara/rack_test/node.rb
def click
if tag_name == 'a'
method = self["data-method"] if driver.options[:respect_data_method]
method ||= :get
driver.follow(method, self[:href].to_s)
elsif (tag_name == 'input' and %w(submit image).include?(type)) or
((tag_name == 'button') and type.nil? or type == "submit")
Capybara::RackTest::Form.new(driver, form).submit(self)
end
end
The tag_name is probably not a link -- because it's a button we're clicking -- so it falls to the elsif. It's definitely an input tag with type == "submit", so then let's see what Capybara::RackTest::Form does:
lib/capybara/rack_test/form.rb
def submit(button)
driver.submit(method, native['action'].to_s, params(button))
end
Ok then. driver is probably the Rack::Test driver for Capybara. What's that doing?
lib/capybara/rack_test/driver.rb
def submit(method, path, attributes)
browser.submit(method, path, attributes)
end
What is this mysterious browser? It's defined in the same file thankfully:
def browser
#browser ||= Capybara::RackTest::Browser.new(self)
end
Let's look at what this class's submit method does.
lib/capybara/rack_test/browser.rb
def submit(method, path, attributes)
path = request_path if not path or path.empty?
process_and_follow_redirects(method, path, attributes, {'HTTP_REFERER' => current_url})
end
process_and_follow_redirects does what it says on the box:
def process_and_follow_redirects(method, path, attributes = {}, env = {})
process(method, path, attributes, env)
5.times do
process(:get, last_response["Location"], {}, env) if last_response.redirect?
end
raise Capybara::InfiniteRedirectError, "redirected more than 5 times, check for infinite redirects." if last_response.redirect?
end
So does process:
def process(method, path, attributes = {}, env = {})
new_uri = URI.parse(path)
method.downcase! unless method.is_a? Symbol
if new_uri.host
#current_host = "#{new_uri.scheme}://#{new_uri.host}"
#current_host << ":#{new_uri.port}" if new_uri.port != new_uri.default_port
end
if new_uri.relative?
if path.start_with?('?')
path = request_path + path
elsif not path.start_with?('/')
path = request_path.sub(%r(/[^/]*$), '/') + path
end
path = current_host + path
end
reset_cache!
send(method, path, attributes, env.merge(options[:headers] || {}))
end
Time to break out the debugger and see what method is here. Sticking a binding.pry before the final line in that method, and a require 'pry' in the test. It turns out method is :post and, for interest's sake, new_uri is a URI object with our remote form's URL.
Where's this post method coming from? method(:post).source_location tells me:
["/Users/ryan/.rbenv/versions/1.9.3-p374/lib/ruby/1.9.1/forwardable.rb", 199]
That doesn't seem right... Does Capybara have a def post somewhere?
capybara (master)★ack "def post"
lib/capybara/rack_test/driver.rb
76: def post(*args, &block); browser.post(*args, &block); end
Cool. We know that browser is aCapybara::RackTest::Browser` object. The class beginning gives the next hint:
class Capybara::RackTest::Browser
include ::Rack::Test::Methods
I know that Rack::Test::Methods comes with a post method. Time to dive into that gem.
lib/rack/test.rb
def post(uri, params = {}, env = {}, &block)
env = env_for(uri, env.merge(:method => "POST", :params => params))
process_request(uri, env, &block)
end
Ignoring env_for for the time being, what does process_request do?
lib/rack/test.rb
def process_request(uri, env)
uri = URI.parse(uri)
uri.host ||= #default_host
#rack_mock_session.request(uri, env)
if retry_with_digest_auth?(env)
auth_env = env.merge({
"HTTP_AUTHORIZATION" => digest_auth_header,
"rack-test.digest_auth_retry" => true
})
auth_env.delete('rack.request')
process_request(uri.path, auth_env)
else
yield last_response if block_given?
last_response
end
end
Hey, #rack_mock_session looks interesting. Where's that defined?
rack-test (master)★ack "#rack_mock_session ="
lib/rack/test.rb
40: #rack_mock_session = mock_session
42: #rack_mock_session = MockSession.new(mock_session)
In two places, very close to each other. What's on and around these lines?
def initialize(mock_session)
#headers = {}
if mock_session.is_a?(MockSession)
#rack_mock_session = mock_session
else
#rack_mock_session = MockSession.new(mock_session)
end
#default_host = #rack_mock_session.default_host
end
Ok then, so it ensures it is a MockSession object. What's MockSession and how is its request method defined?
def request(uri, env)
env["HTTP_COOKIE"] ||= cookie_jar.for(uri)
#last_request = Rack::Request.new(env)
status, headers, body = #app.call(#last_request.env)
headers["Referer"] = env["HTTP_REFERER"] || ""
#last_response = MockResponse.new(status, headers, body, env["rack.errors"].flush)
body.close if body.respond_to?(:close)
cookie_jar.merge(last_response.headers["Set-Cookie"], uri)
#after_request.each { |hook| hook.call }
if #last_response.respond_to?(:finish)
#last_response.finish
else
#last_response
end
end
I'm going to go right ahead here and assume #app is the Rack application stack. By calling the call method, the request is routed directly to this stack, rather going out to the world.
I conclude that this behaviour looks like its intentional and that I can indeed rely on it being that way.

Rails sanitize remove default allowed tags

How would I use sanitize, but tell it to disallow some enabled by default tags? The documentation states that I can put this in my application.rb
config.after_initialize do
ActionView::Base.sanitized_allowed_tags.delete 'div'
end
Can I instead pass this as an argument to sanitize?
Yes you can specify which tags and attributes to allow on a per-call basis. From the fine manual:
Custom Use (only the mentioned tags and attributes are allowed, nothing else)
<%= sanitize #article.body, :tags => %w(table tr td), :attributes => %w(id class style) %>
But the problem with that is that :tags has to include all the tags you want to allow.
The sanitize documentation says to
See ActionView::Base for full docs on the available options.
but the documentation is a lie, ActionView::Base says nothing about the available options.
So, as usual, we have to go digging through the source and hope they don't silently change the interface. Tracing through the code a bit yields this:
def tokenize(text, options)
options[:parent] = []
options[:attributes] ||= allowed_attributes
options[:tags] ||= allowed_tags
super
end
def process_node(node, result, options)
result << case node
when HTML::Tag
if node.closing == :close
options[:parent].shift
else
options[:parent].unshift node.name
end
process_attributes_for node, options
options[:tags].include?(node.name) ? node : nil
else
bad_tags.include?(options[:parent].first) ? nil : node.to_s.gsub(/</, "<")
end
end
The default value for options[:tags] in tokenize and the way options[:tags] is used in process_node are of interest and tell us that if options[:tags] has anything then it has to include the entire set of allowed tags and there aren't any other options for controlling the tag set.
Also, if we look at sanitize_helper.rb, we see that sanitized_allowed_tags is just a wrapper for the allowed_tags in the whitelist sanitizer:
def sanitized_allowed_tags
white_list_sanitizer.allowed_tags
end
You should be able to add your own helper that does something like this (untested off-the-top-of-my-head code):
def sensible_sanitize(html, options)
if options.include? :not_tags
options[:tags] = ActionView::Base.sanitized_allowed_tags - options[:not_tags]
end
sanitize html, options
end
and then you could
<%= sensible_sanitize #stuff, :not_tags => [ 'div' ] %>
to use the standard default tags except for <div>.
I know you're looking for a way to pass the tags in, and for that mu's answer looks good!
I was keen to set them up globally which was trickier than I'd hoped as ActionView::Base has overridden sanitized_allowed_tags= to append rather than replace!
I ended up with the following to my application.rb:
SANITIZE_ALLOWED_TAGS = %w{a ul ol li b i}
config.after_initialize do
ActionView::Base.sanitized_allowed_tags.clear
ActionView::Base.sanitized_allowed_tags += SANITIZE_ALLOWED_TAGS
end

Resources