Rails: Request-based (& Database-based) Routing - ruby-on-rails

I am trying to get rid of some scope-prefixes I am currently using in my app.
At the moment my Routes look like this (simplified example):
scope 'p'
get ':product_slug', as: :product
end
scope 't' do
get ':text_slug', as: :text
end
which for example generates these paths:
/p/car
/t/hello-world
Now I want the paths to work without the prefixed letters (p & t). So I restrict the slugs to the existing database entries (which btw works great):
text_slugs = Text.all.map(&:slug)
get ':text_slug', as: :text, text_slug: Regexp.new( "(#{text_slugs.join('|')})"
product_slugs = Product.all.map(&:slug)
get ':product_slug', as: :product, product_slug: Regexp.new( "(#{product_slugs.join('|')})"
The problem:
This is a multi-tenant app which means that someones text_slug could be another ones product_slug and vice versa. That's why I have to filter the slugs by the current site (by domain).
A solution would look like this:
text_slugs = Site.find_by_domain(request.host).texts.all.map(&:slug)
get ':text_slug', as: :text, text_slug: Regexp.new( "(#{text_slugs.join('|')})"
But request isn't available in routes.rb and I everything I tried won't work.
The direct call to Rack::Request needs the correct env variable which doesn't seem to be present in Application.routes, otherwise this could work:
req = Rack::Request.new(env)
req.host
I really tried alot and am thankful for any hint!

You may be able to use advanced constraints for this: http://guides.rubyonrails.org/routing.html#advanced-constraints.
class SlugConstraint
def initialize(type)
#type = type
end
def matches?(request)
# Find users subdomain and look for matching text_slugs - return true or false
end
end
App::Application.routes.draw do
match :product_slug => "products#index", :constraints => SlugConstraint.new(:product)
match :tag_slug => "tags#index", :constraints => SlugConstraint.new(:tag)
end
BTW - You may run into problems with testing, but that's another issue...

Related

ElegantRails - Multiple Routes to one Controller Action

I'm wondering if there is a more cleaner or elegant way of translating multiple routes to one controller action using Rails.
#routes.rb
get 'suggestions/proxy', to: 'suggestions#index'
get 'suggestions/aimee', to: 'suggestions#index'
get 'suggestions/arty', to: 'suggestions#index'
...
#suggestion_controller.rb
case request.env['PATH_INFO']
when '/suggestions/proxy'
#suggestions = Suggestion.all.where(:suggestion_type => 'proxy')
when '/suggestions/aimee'
#suggestions = Suggestion.all.where(:suggestion_type => 'aimee')
when '/suggestions/arty'
#suggestions = Suggestion.all.where(:suggestion_type => 'arty')
...
else
#suggestions = Suggestion.all
end
I've read this this post, but I kept getting errors when using it.
It's not a big deal if there's not a lot to be done here. I'm building a website on a video game I like playing called Dirty Bomb and there is a total of 19 mercenaries that need to be listed, so that's why I wanted a more cleaner way of doing this.
Thanks.
Absolutely there is. You can use a parameter directly in your route. Even further, you can then use that parameter directly in your query, rather than using a case statement.
#routes.rb
get 'suggestions/:type', to: 'suggestions#index'
# suggestions_controller.rb
def index
#suggestions = Suggestion.where(suggestion_type: params[:type])
end
It's always a better practice to base your controller actions after parameters, rather than doing any interpretation of the path or request objects.
Hope it works!

Overloading a route one and two variables in Rails 4

I recently upgraded from rails 3 to rails 4 on one of our legacy apps, the problem is that rails 4 doesn't allow the same path name on two URLs even if they take a different number of arguments.
E.g. we used to do this:
get "object/:id/data/:dataid" => "object#data", as: :object_data
get "object/:id/data/:dataid/:extra" => "object#data", as: :object_data
but in rails 4 having two object_data_paths is not allowed. I'm wanting to accomplish the same thing as before. It seems like one method would be to call the second one something new, but use the same method, i.e.:
get "object/:id/data/:dataid/:extra" => "object#data", as: :object_data_extra
But this does seem like a worse solution than before. Any other ways I can do this? Thoughts on why we have to do this?
There are two solutions. The first is to use an optional path segment:
get "object/:id/data/:dataid(/:extra)" => "object#data", as: :object_data
The other is to wrap it in a helper:
get "object/:id/data/:dataid" => "object#data", as: :object_data_1
get "object/:id/data/:dataid/:extra" => "object#data", as: :object_data_2
# And in application_helpers.rb or somewhere similar
def object_data_path(id, dataid, extra=nil)
if extra
object_data_2_path(id, dataid, extra)
else
object_data_1_path(id, dataid)
end
end

How to have custom urls that map to values in the db?

I want to have urls like this:
www.example.com/topic1/...
www.example.com/topic2/...
www.example.com/topic3/...
And these should be served using the TopicController.
The values topic1, topic2, topic3, .. are coming from the table in the database (topics).
Is this possible?
What will my route look like then? These topics will be added ofcourse, it isn't something that is static in nature.
Try:
match '*a/' => 'topic#show' # assume the action is show
params[:a] will equal topic1 etc.
The closest solution I can think of would be to define a route such as
match "/topic/:name" => "topic#process_topic"
and the corresponding action in the TopicController
def process_topic
#topic = Topic.find_by_name(params[:name])
case #topic.name
when topic1
...
when topic2
...
end
end

Parsing custom feed elements using FeedZirra

Is there a way to parse feed's custom elements? Not feed entries', feed's custom elements. I know there is a way to do the same for the entries. Like,
Feedzirra::Feed.add_common_feed_entry_element("wfw:commentRss", :as => :comment_rss)
feed = Feedzirra::Feed.parse(some_atom_xml)
feed.entries.first.comment_rss # => wfw:commentRss is now parsed!
I want to be able to achieve the same for the feed object. Something like,
Feedzirra::Feed.add_common_feed_element("geo:lat", :as => :latitudes)
feed = Feedzirra::Feed.fetch_and_parse(“somerss”)
feed.latitudes # => 44.022448
Is there a way? Or does this requires writing a patch for FeedZirra?
It's a bit late, but more people might be looking for an answer.
Putting the following line in a file in your config/initializers seems to work:
Feedzirra::Parser::RSS.element :latitudes
According to the new http://feedjira.com/extending.html
# Add the generator attribute to all feed types
Feedjira::Feed.add_common_feed_element('generator')
Feedjira::Feed.fetch_and_parse("http://www.pauldix.net/atom.xml").generator # => 'TypePad'
# Add some GeoRss information
Feedjira::Feed.add_common_feed_entry_element('geo:lat', :as => :lat)
Feedjira::Feed.fetch_and_parse("http://www.earthpublisher.com/georss.php").entries.each do |e|
p "lat: #[e.lat}, long: #{e.long]"
end

How to test a scope in Rails 3

What's the best way to test scopes in Rails 3. In rails 2, I would do something like:
Rspec:
it 'should have a top_level scope' do
Category.top_level.proxy_options.should == {:conditions => {:parent_id => nil}}
end
This fails in rails 3 with a "undefined method `proxy_options' for []:ActiveRecord::Relation" error.
How are people testing that a scope is specified with the correct options? I see you could examine the arel object and might be able to make some expectations on that, but I'm not sure what the best way to do it would be.
Leaving the question of 'how-to-test' aside... here's how to achieve similar stuff in Rails3...
In Rails3 named scopes are different in that they just generate Arel relational operators.
But, investigate!
If you go to your console and type:
# All the guts of arel!
Category.top_level.arel.inspect
You'll see internal parts of Arel. It's used to build up the relation, but can also be introspected for current state. You'll notice public methods like #where_clauses and such.
However, the scope itself has a lot of helpful introspection public methods that make it easier than directly accessing #arel:
# Basic stuff:
=> [:table, :primary_key, :to_sql]
# and these to check-out all parts of your relation:
=> [:includes_values, :eager_load_values, :preload_values,
:select_values, :group_values, :order_values, :reorder_flag,
:joins_values, :where_values, :having_values, :limit_value,
:offset_value, :readonly_value, :create_with_value, :from_value]
# With 'where_values' you can see the whole tree of conditions:
Category.top_level.where_values.first.methods - Object.new.methods
=> [:operator, :operand1, :operand2, :left, :left=,
:right, :right=, :not, :or, :and, :to_sql, :each]
# You can see each condition to_sql
Category.top_level.where_values.map(&:to_sql)
=> ["`categories`.`parent_id` IS NULL"]
# More to the point, use #where_values_hash to see rails2-like :conditions hash:
Category.top_level.where_values_hash
=> {"parent_id"=>nil}
Use this last one: #where_values_hash to test scopes in a similar way to #proxy_options in Rails2....
Ideally your unit tests should treat models (classes) and instances thereof as black boxes. After all, it's not really the implementation you care about but the behavior of the interface.
So instead of testing that the scope is implemented in a particular way (i.e. with a particular set of conditions), try testing that it behaves correctly—that it returns instances it should and doesn't return instances it shouldn't.
describe Category do
describe ".top_level" do
it "should return root categories" do
frameworks = Category.create(:name => "Frameworks")
Category.top_level.should include(frameworks)
end
it "should not return child categories" do
frameworks = Category.create(:name => "Frameworks")
rails = Category.create(:name => "Ruby on Rails", :parent => frameworks)
Category.top_level.should_not include(rails)
end
end
end
If you write your tests in this way, you'll be free to re-factor your implementations as you please without needing to modify your tests or, more importantly, without needing to worry about unknowingly breaking your application.
This is how i check them. Think of this scope :
scope :item_type, lambda { |item_type|
where("game_items.item_type = ?", item_type )
}
that gets all the game_items where item_type equals to a value(like 'Weapon') :
it "should get a list of all possible game weapons if called like GameItem.item_type('Weapon'), with no arguments" do
Factory(:game_item, :item_type => 'Weapon')
Factory(:game_item, :item_type => 'Gloves')
weapons = GameItem.item_type('Weapon')
weapons.each { |weapon| weapon.item_type.should == 'Weapon' }
end
I test that the weapons array holds only Weapon item_types and not something else like Gloves that are specified in the spec.
Don't know if this helps or not, but I'm looking for a solution and ran across this question.
I just did this and it works for me
it { User.nickname('hello').should == User.where(:nickname => 'hello') }
FWIW, I agree with your original method (Rails 2). Creating models just for testing them makes your tests way too slow to run in continuous testing, so another approach is needed.
Loving Rails 3, but definitely missing the convenience of proxy_options!
Quickly Check the Clauses of a Scope
I agree with others here that testing the actual results you get back and ensuring they are what you expect is by far the best way to go, but a simple check to ensure that a scope is adding the correct clause can also be useful for faster tests that don't hit the database.
You can use the where_values_hash to test where conditions. Here's an example using Rspec:
it 'should have a top_level scope' do
Category.top_level.where_values_hash.should eq {"parent_id" => nil}
end
Although the documentation is very slim and sometimes non-existent, there are similar methods for other condition-types, such as:
order_values
Category.order(:id).order_values
# => [:id]
select_values
Category.select(:id).select_values
# => [:id]
group_values
Category.group(:id).group_values
# => [:id]
having_values
Category.having(:id).having_values
# => [:id]
etc.
Default Scope
For default scopes, you have to handle them a little differently. Check this answer out for a better explanation.

Resources