I'm trying to sort out the routing for a multinational web store which only has a presence in certain countries. Each store is different and has a different catalogue of products and there is also a world-wide store for all other countries. I have set up Nginx to prepend the two letter country code from an lookup of the user's IP address so that my Rails app can figure which store to direct the visitor to. I then use Sven Fuch's excellent routing-filter to capture that code, do a lookup against a global SHOPS object and if a match is found then the country code is used, otherwise they get the default (world-wide) store. My routing filter currently looks like this:
module RoutingFilter
class Country < Filter
countries_pattern ||= %r(^/(?i)([a-zA-Z]{2})(?=/|$))
def around_recognize(path, env, &block)
country = "#{extract_segment!(countries_pattern, path)}".upcase
yield(path, env).tap do |params|
params[:shop] = SHOPS.fetch(country.to_sym) || DEFAULT_SHOP
end
end
def around_generate(params, &block)
puts params
shop = params.delete(:shop)
yield.tap do |result|
prepend_segment!(result, shop[:country_code]) if shop
end
end
end
end
Now the curious thing is, the params collection does not contain a :shop param when the around_generate method is executed. My code is directly based on the pagination filter included with the routing-filter gem (I'm not using the locale filter as each of these stores is also multilingual - i18n is handled using accept-language header instead). The original pagination filter by Sven Fuchs looks like this:
module RoutingFilter
class Pagination < Filter
PAGINATION_SEGMENT = %r(/page/([\d]+)/?$)
def around_recognize(path, env, &block)
page = extract_segment!(PAGINATION_SEGMENT, path)
yield(path, env).tap do |params|
params[:page] = page.to_i if page
end
end
def around_generate(params, &block)
page = params.delete(:page)
yield.tap do |result|
append_segment!(result, "page/#{page}") if append_page?(page)
end
end
protected
def append_page?(page)
page && page.to_i != 1
end
end
end
In my filter, shop = params.delete(:shop) results in a Nil object error and I can see from "puts params" that it is indeed not present. Does anyone have any suggestions as to why I'm unable to store and retrieve the :shop param?
Edit: I should mention that I have checked that the parameter gets set correctly in around_recognize - a "puts" of the params collection here does indeed contain the correct :shop object.
If you don't pass a :shop param to url_for (or whatever url generation helper you use here) it won't be passed to around_generate either. around_generate wraps around the url generation part of the routing system.
The Pagination filter assumes the same, e.g. it would be called like blog_posts_path(:page => 2).
But maybe that's not what you want. If you have a look at the Locale filter then this assumes that you sometimes pass a locale to the url helper but sometimes you don't. If :locale is not given it will look it up from I18n.locale which is the current locale set for this request. Maybe you want something similar here?
You could also have a look at the controller's default_url_options. IIRC you can set a default option here, too, so maybe this could work for you. I'm not using this approach anywhere though, so I'm just guessing.
HTH
Related
I need to handle a particular case of generating email views with URLs constructed from non-persisted data.
Example : assume my user can create posts, and that triggers a post creation notification email, I'd like to send the user an example of fake post creation. For this, I am using a FactoryGirl.build(:post) and passing this to my PostMailer.notify_of_creation(#post)
In everyday Rails life, we use the route url_helpers by passing as argument the model itself, and the route generator will automatically convert the model into its ID to be used for the route URL generation (in article_path(#article), the routes helper converts #article into #article.id for constructing the /articles/:id URL.
I believe it is the same in ActiveRecord, but anyways in Mongoid, this conversion fails if the model is not persisted (and this is somewhat nice as it prevents the generation of URLs that may not correspond to actual data)
So in my specific case, URL generation crashes as the model is not persisted:
<%= post_url(#post_not_persisted) %>
crashes with
ActionView::Template::Error: No route matches {:action=>"show", :controller=>"posts", :post_id=>#<Post _id: 59b3ea2aaba9cf202d4eecb6 ...
Is there a way I can bypass this limitation only in a very specific scope ? Otherwise I could replace all my resource_path(#model) by resource_path(#model.id.to_s) or better #model.class.name but this doesn't feel like the right situation...
EDIT :
The main problem is
Foo.new.to_param # => nil
# whereas
Foo.new.id.to_s # => "59b528e8aba9cf74ce5d06c0"
I need to force to_param to return the ID (or something else) even if the model is not persisted. Right now I'm looking at refinements to see if I can use a scoped monkeypatch but if you have better ideas please be my guest :-)
module ForceToParamToUseIdRefinement
refine Foo do
def to_param
self.class.name + 'ID'
end
end
end
However I seem to have a small scope problem when using my refinement, as this doesn't bubble up as expected to url_helpers. It works fine when using te refinement in the console though (Foo.new.to_param # => 59b528e8aba9cf74ce5d06c0)
I found a way using dynamic method override. I don't really like it but it gets the job done. I am basically monkeypatching the instances I use during my tests.
To make it easier, I have created a class method example_model_accessor that basically behaves like attr_accessor excepts that the setter patches the #to_param method of the object
def example_model_accessor(model_name)
attr_reader model_name
define_method(:"#{model_name}=") do |instance|
def instance.to_param
self.class.name + 'ID'
end
instance_variable_set(:"##{model_name}", instance)
end
end
Then in my code I can just use
class Testing
example_model_accessor :message
def generate_view_with_unpersisted_data
self.message = FactoryGirl.build(:message)
MessageMailer.created(message).deliver_now
end
end
# views/message_mailer/created.html.erb
...
<%= message_path(#message) %> <!-- Will work now and output "/messages/MessageID" ! -->
I working on project using Rails 4.1.6 now. And I have strange problem. Method to_param for my models (Product, Category) sometimes not calling. I use it for SEO-friendly urls.
Here is my to_param method for Category model:
def to_param
puts 'I am in Category to_param'
"#{id}-#{title.to_slug.normalize.to_s}"
end
I use puts for find out is this method working or no. So, when my urls looks good (/categories/39-средства-дезинфекции) I can see the string 'I am in Category to_param' on my server console. This is correct case and all it's great.
But sometimes I have urls like /categories/39 for the same objects. When I look into console for this case, I don't see any prints from my to_param method form Category model.
These two cases I have on the same pages, same views and using the same helpers for category url (category_path).
Most complicated for this situation is that I can't reproduce this bug and don't see any regularity. For the same objects I have correct urls most of times, but sometimes it's not. If I restart rails server and refresh browser with clear cache – problem may out and urls will be correct again.
During my debug and research I found source code for base class. But I can't see there any reasons for the situation described above.
def to_param(method_name = nil)
if method_name.nil?
super()
else
define_method :to_param do
if (default = super()) &&
(result = send(method_name).to_s).present? &&
(param = result.squish.truncate(20, separator: /\s/, omission: nil).parameterize).present?
"#{default}-#{param}"
else
default
end
end
end
end
Also I can tell that this problem was appear, when I used FriendlyID before, using regex for clear and build slugs, and now for babosa gem. So, I think the problem is my to_param sometimes not calling for my model.
So, I found the reason of this behaviour. Now it's resolved!
The reason was I have redefined to_param for Product and Category in my ActiveAdmin files:
before_filter do
Product.class_eval do
def to_param
puts "I am in admin Product to_param"
id.to_s
end
end
Category.class_eval do
def to_param
puts "I am in admin Category to_param"
id.to_s
end
end
end
So, when I was log in Admin panel and go to Product page – "bug" will appear on front-end views.
So, I need to remove Product.class_eval and Category.class_eval blocks from my admin classes.
In Rails the default routes use the internal database id to identify the resource, so you end up with routes like:
/user/1/widget/4
It's possible to change these to use something other than :id easily enough so that you could have routes like:
/user/bob/widget/favorites
But is there a way to have both available? I ask because in my case I'm using the route to create a unique id for use with an external service, but I'd like them to be based on a field other than id because it's more useful to pass these alternative ids to the external service.
I can of course build something custom, but we currently have some code that works as follows (with other convenience functions on top; this is the core functionality) to get most of the functionality I would have to build 'for free' from Rails:
class PathIdParser
def initialize
#context = Application.routes
end
def parse(path)
#context.recognize_path(path)
end
def build(route, params)
#context.named_routes[route].format(params)
end
end
Obviously the build function is easy enough to work with to use other routes by just changing the values passed into the params hash, but is there a way I can get parse to use these alternative fields to look up resources by, since recognize_path seems to work based on the values returned by to_param.
In routes.rb
get 'user/:username/widget/favourites', to: 'users#favourites'
This would route 'user/bob/widget/favourites' to the favourites action of the UsersController and you could access the username via
#username = params[:username]
Use the method to_param() in your model.
It returns a String, which Action Pack uses for constructing an URL to this object. The default implementation returns this record’s id as a String, or nil if this record’s unsaved.
class User < ActiveRecord::Base
def to_param
name
end
end
user = User.find_by_name('Richard')
user_path(user) # => "/users/Richard"
I'm trying to figure out how to obfuscate the ids of my records in rails.
For example: a typical path might look like http://domain/records/1, so it's pretty easy for people to deduce how much traffic the site is getting if they just create a new record.
One solution that I've used is to hash the id with a salt, but since I'm not sure whether that function is bijective, I end up storing it in another column in my database and double check for uniqueness.
Another option I was thinking about was generating a random hash and storing that as another column. If it isn't unique ... just generate another one.
What's the best way of doing this?
You could use the built-in OpenSSL library to encrypt and decrypt your identifiers, that way you would only need to overwrite to_param on your models. You'll also need to use Base64 to convert the encrypted data into plain text. I would stick this in a module so it can be reused:
require 'openssl'
require 'base64'
module Obfuscate
def self.included(base)
base.extend self
end
def cipher
OpenSSL::Cipher::Cipher.new('aes-256-cbc')
end
def cipher_key
'blah!'
end
def decrypt(value)
c = cipher.decrypt
c.key = Digest::SHA256.digest(cipher_key)
c.update(Base64.decode64(value.to_s)) + c.final
end
def encrypt(value)
c = cipher.encrypt
c.key = Digest::SHA256.digest(cipher_key)
Base64.encode64(c.update(value.to_s) + c.final)
end
end
So now your models would need to look something like this:
class MyModel < ActiveRecord::Base
include Obfuscate
def to_param
encrypt id
end
end
Then in your controller when you need to find a record by the encrypted id, you would use something like this:
MyModel.find MyModel.decrypt(params[:id])
If you're looking to encrypt/decrypt ids without storing them in the database, this is probably the easiest way to go.
Instead of numeric ids, use some kind of friendly url or human readable slug. There are lots of tools to choose from in this department. Not only are they more friendly to your users, but well chosen slugs can give a nice advantage with search engines.
Here's a gem that keeps it numeric, requires no database migrations, and no routing changes: https://github.com/namick/obfuscate_id
I've found that this gem doesn't work in concert with some other gems, notably paper_trail. This is because of the way it replaces the find method, and paper_trail causes find to be called with the actual record id.
So I've been using the gem's "scatter_swap" functionality, but not the rest of it. Here's the model:
require 'obfuscate_id/scatter_swap'
class Page < ActiveRecord::Base
# This is a random number that, if changed, will invalidate all existing URLs. Don't change it!
##obfuscate_spin = # random number here, which is essentially the encryption key
##
# Generate URL parameter to be used in the URL as the "id"
def to_param
# Use the obfuscate_id gem's class to "spin" the id into something obfuscated
spun_id = ScatterSwap.hash(self.id, ##obfuscate_spin)
# Throw any additional attributes in here that are to be included in the URL.
"#{spun_id} #{name}".parameterize
end
def self.find_by_slug!(slug)
spun_id = slug[/^[0-9]+/]
begin
find_by_id! ScatterSwap.reverse_hash(spun_id, ##obfuscate_spin)
rescue ActiveRecord::RecordNotFound => e
raise ActiveRecord::RecordNotFound, "Couldn't find matching Page."
end
end
end
And in the controller:
class PagesController < InheritedResources::Base
# Find the page using its URL slug
before_filter :find_page, except: [:index, :create, :new]
def find_page
#page = Page.find_by_slug! params[:id]
# If the URL doesn't match exactly, and this is a GET.
# We'll redirect to the new, correct URL, but if this is a non-GET, let's let them finish their request instead.
if params[:id] != #page.to_param && request.get?
redirect_to url_for({ id: #page.to_param }), status: 301
end
end
end
As an alternative to the redirection that takes place there, you could simply include a canonical URL in the page. The redirection has the bug of ignoring any query parameters in the URL. This was not a problem for my project, as I didn't have any. But a canonical URL would be better.
It's pretty easy to generate unique random identifiers for your records either using a randomized string generator or a simple call to Digest::SHA1.hexdigest which produces reasonably random and cryptographically unique results.
For instance, you can create a secondary column called ident or unique_id that stores your public identifiers. You can then over-write to_param to use this instead:
class MyModel < ActiveRecord::Base
before_create :assign_ident
def self.from_param(ident)
find_by_ident(ident)
end
def to_param
self.ident
end
protected
def assign_ident
self.ident = Digest::SHA1.hexdigest(SecureRandom.random_number(1<<256).to_s)
end
end
Theoretically there is a chance of collision on SHA1 but the odds are so astronomically low you're more liable to have a software crash because of a memory error or hardware malfunction. You can test this to see if it suits your needs by generating a few billion identities to see if they ever collide, which they shouldn't. A 256-bit random number should provide a sufficient amount of data for the SHA1 algorithm to chew on.
After reading through #siannopollo's post, I created a Gem based on the idea of his post (but with some improvements): https://github.com/pencil/encrypted_id
Just because it hasn't been mentioned here: You could simply use UUIDs (wikipedia article)
There are multiple ways of using UUID as primary keys in Rails, depending on your Rails version and database engine. It's easy to find.
Just as a possibility, in case you depend too much on your existing integer primary key, you can also just add a UUID to your table and make your model use it automatically when it comes to generating URLs by overwriting Model#to_param more details in the docs
In some book, it is recommended that to_param is changed to
class Story < ActiveRecord::Base
def to_param
"#{id}-#{name.gsub(/\W/, '-').downcase}"
end
end
so that the URL is
http://www.mysite.com/stories/1-css-technique-blog
instead of
http://www.mysite.com/stories/1
so that the URL is more search engine friendly.
So probably to_param() doesn't need to be used by other parts of Rails that changing it may have any side effect? Or maybe the only purpose is to construct a URL for linking?
Another thing is, won't it require to limit the URL size to be less than 2k in length -- will it choke IE if it is more than 2k or maybe the part more than 2k is just ignored by IE and so the URL still works. It might be better to be limited to 30 or 40 characters or something that will make the URL not exceedingly long.
Also, the ri doc of to_param:
class User < ActiveRecord::Base
def to_param # overridden
name
end
end
if to_param is changed like that, then the link actually won't work, as
http://www.mysite.com/stories/1-css-technique-blog
will work, but
http://www.mysite.com/stories/css-technique-blog
will not work as the ID is missing. Are there other ways to change the to_param method?
Update: on second thought, maybe
http://www.mysite.com/stories/css-technique-blog
won't work well if there are many webpages with similar title. but then
http://www.mysite.com/user/johnchan
will work. Will it be params[:id] being "johnchan"? So then we will use
user = User.find_by_login_name(params[:id])
to get the user. So it just depends on how we use the param on the URL.
C:\ror>ri ActiveRecord::Base#to_param
-------------------------------------------- ActiveRecord::Base#to_param
to_param()
------------------------------------------------------------------------
Returns a String, which Action Pack uses for constructing an URL to
this object. The default implementation returns this record's id as
a String, or nil if this record's unsaved.
For example, suppose that you have a User model, and that you have
a +map.resources :users+ route. Normally, +user_path+ will
construct a path with the user object's 'id' in it:
user = User.find_by_name('Phusion')
user_path(user) # => "/users/1"
You can override +to_param+ in your model to make +user_path+
construct a path using the user's name instead of the user's id:
class User < ActiveRecord::Base
def to_param # overridden
name
end
end
user = User.find_by_name('Phusion')
user_path(user) # => "/users/Phusion"
If you want to make your url more search engine friendly, you can use the friendly_id gem which makes exactly what you want. Is the easier way I've found to generate search engine friendly permalinks.