How can I parse the params from a custom URL?
Lets say I have a User class that implements a to_param and a from_param method and I have a backend admin where a "customer" can insert an URL from a user (profile) page (e.g. http://localhost:3000/users/JohnDoe-123security456-123 where 123 is the ID).
Is it possible to generate a custom params object or something similar to parse the id from the url?
My goal is to reuse the existing logic instead of creating another regex.
I know I could do something like: "http://localhost:3000/users/JohnDoe-123security456-123/custom_action?abc=def".gsub(/^.*\/users\//, '').gsub(/\/.*$/,'') (or something more suffisticated) to get the id.
Here is the pseudocode of what I try to achieve.
class User < ActiveRecord::Base
def to_param
"#{name}-#{security-token}-#{id}"
end
def self.id_from_param(param_id)
param_id.to_s.gsub(/.*-/,'')
end
end
class AdminUserController < ActionController::Base
def search
url = params[:url]
parsed_params = some_method_that_extracts_the_params_from_url(url) # (1)
user_id = User.id_from_param(parsed_params[:id])
end
end
Related
Let's say I have two models: Client and Product
The "username" and "email" of Client should be "unique index", as "serialnumber" of Product
When the user is typing on the form field that is unique index, I have an onblur function that sends a request to the controller with the attribute name and attribute value. If a value exists, the user is immediately informed.
In ClientController, I wrote a function that checks if it's unique or not and returns -2 for error, -1 for not exists or a positive number (the id) if exists.
def unique
if params[:attrName].blank? or params[:attrValue].blank?
id = "-2"
else
cli = Client.where("#{params[:attrName]} = '#{params[:attrValue]}'").first
if cli != nil
id = cli["id"]
else
id = "-1"
end
end
render :json => {
:id => id
}
end
This is not good for many reasons (SQL Injection vulnerability, violation of DRY, as each controller would have basically the same method.
I'm thinking of writing the "unique" function inside ApplicationController, but as you saw above, I should be able to call "Client.where" if it's a client, or "Product.where" if it's a product. How can I build this function the most "generically" possible and the most securily? I'm thinking of raw SQL but I think this is a naive approach.
It would be wise to avoid raw SQL for this.
Would this work?
class ApplicationController < ActionController::Base
def unique
id = if params[:attrName].blank? || params[:attrValue].blank?
-2
elsif found = model_name.where(params[:attrName] => params[:attrValue]).take
found.id
else
-1
end
render json: { id: id }
end
end
You could put that in application_controller.rb then in both of your ClientsController and ProductsController you would define the model_name method:
class ClientsController < ApplicationController
def model_name
Client
end
end
class ProductsController < ApplicationController
def model_name
Product
end
end
This will work but might not be ideal. You might want to let Rails do more of the work by using find to raise if a model exists or not and strong params for validating that the params you need are present.
You can move this to a module and make it return an ActiveRecord relation. the advantage is later you can chain this with other ActiveRecord relations if you wish to, Something like (and note I have used ? in my sql condition, instead of directing giving the param )
#module
module UniqueRecord
module ClassMethods
def unique(params)
where(params)
end
end
def self.included(receiver)
receiver.extend ClassMethods
end
end
and use it in your class
#client.rb
class Client < ActiveRecord::Base
include UniqueRecord
end
#product.rb
class Product < ActiveRecord::Base
include UniqueRecord
end
So now both of your classes has the method unique available.
you can create a hash from the keys and values you get, Ex: you could dynamically create a hash to search email like
hash = {email: 'same#email.com'}
and then call the method
Client.unique(hash)
and if you want to , you can call it by the class name string
'Client'.constantize.unique(hash)
one more thing, it better to return an array of objects (if found) or blank array (if not found) instead of -1, -2. that will make your api consistent. like
Client.unique(hash).to_json
I want to expose my database ids and encode/decode the id with routes helper. For encoding I use Hashids gem.
Now I have:
routes.rb
get 'companies/:id/:year', to: 'company#show', as: 'companies'
company url:
/companies/1/2015
For id encoding I have encode/decode helper methods:
def encode(id)
# encode...
return 'ABC123'
end
def decode(hashid)
# decode...
return 1
end
How I can implemented, that id will be with routes helper converted?
So must show the URL:
/companies/ABC123/2015
and controller must get automatically params with id 1.
Thanks for your answers! But I wont to decode params id without changes in the model or controller. After long consideration, I have decided the params id to manipulate, before controller get params. I manipulate params in routes Constraints.
example helper:
encoding_helper.rb
module EncodingHelper
def encode(id)
# encode...
return 'ABC123'
end
def decode(hashid)
# decode...
return 1
end
end
Create path with a encode id:
companies_path(id: encode(1), year: 2015) # => /companies/ABC123/2015
Manipulate params in routes Constraints:
lib/Constraints/decode_company_id.rb
module Constraints
class DecodeId
extend EncodingHelper
def self.matches?(request)
request.params['id'] = decode(request.params['id']).to_s if request.params['id'].present?
true
end
end
end
config/routes.rb
constraints(Constraints::DecodeId) do
get 'companies/:id/:year', to: 'company#show', as: 'companies'
end
After decode params id with constraints und without manipulation in controller, params id is 1.
You can use the to_param method for this.
#in Company
def to_param
self.encoded_id
end
def encoded_id
self.class.encode_id(self.id)
end
def find_by_encoded_id(encoded_id)
self.find_by_id(self.class.decode_id(encoded_id)
end
#class methods
class << self
def encode_id(id)
#encoding algorithm here
end
def decode_id(encoded_id)
#decoding algorithm here
end
end
this will mean that urls featuring the id of the company will actually use the encoded_id instead, assuming you pass through the company object to a path helper, eg company_path(#company).
Then, in your companies controller, you just need to make sure that you find_by_encoded_id(params[:id]) rather than find_by_id(params[:id]).
Rails router shouldn't be doing any decoding:
The Rails router recognizes URLs and dispatches them to a controller's action.
The logic should belong to the controller.
When your controller receives an encoded response:
#Appropriate controller
def show
Company.decode(params[:id])
end
This work work nicely if you slightly adjust your model method to:
def self.decode(code)
# decode => get id
find(id) #returns Company object
end
you can try this. custom method of friendly id
in model
extend FriendlyId
friendly_id :decode
# Try building a slug based on the following fields in
# increasing order of specificity.
def decode
conditional_check(self.id)
end
private
def conditional_check(id)
return "ABC123" if id == 1
end
Right now if I create a URL for a model show action I simply call:
link_to model_instance
which creates something like this the when model is User and the id is 1:
/user/1
I like to customize that behavior without having to go through all instances in the codebase where a URL for such a model is generated. The motivation behind the change is avoiding rolling id's in the URL that lets anybody discover other entries by simply increasing the id. So something like
/user/88x11bc1200
Is there a place where I can simply override how a URL for selected models are generated? I am using RoR 4.x
There are essentially two places you'll have to update.
In the model
class User < ActiveRecord::Base
# Override the to_param method
def to_param
# Whatever your field is called
non_rolling_id
end
end
In the controller
class UsersController < ApplicationController
def show
# Can't use `find` anymore, but will still come over as `id`
#user = User.find_by_non_rolling_id(params[:id])
end
end
I created some scaffolding to manage audio clips which are going to be organized by index:
rails generate scaffold Clips name:string
I uploaded all the clips to my file server, and added them to the db using the auto generated rails control panel.
Now, I need to be able to access them, so I added a url method to the model:
class Clip < ActiveRecord::Base
def self.url
"http://example.file_server.com/audio-clips/#{#id}.mp3"
end
end
Now, in the controller than runs the site itself, calling this method looks like it outputs everything but the id....
class TwilioController < ApplicationController
def index
Twilio::TwiML::Response.new do |r|
#response = r.play Clip.where(name: "root").url
end
render :xml => #response
end
end
Outputs:
<Response>
<play>http://example.file_server.com/audio-clips/.mp3</play>
</Response>
How can I get this thing to insert the id into the URL string?
A few things, one, you defined url as self.url, which makes it a class level method. I'm guessing you didn't want to do that.
Also, don't use id as an instance variable, use its generated accessor method:
class Clip < ActiveRecord::Base
def url
"http://example.file_server.com/audio-clips/#{id}.mp3"
end
end
Also, you are calling url right after the where call, which returns a relation. You'll want to do something like:
Twilio::TwiML::Response.new do |r|
#response = r.play Clip.where(name: "root").first.url
end
But that depends more on what you are doing. If you expect there to be several results, you'll have to handle it differently. Also beware it may return no results...
I have a little database with a movies that I saw. Now when I want to display a detail of a movie, so the profile of wanted movie is on the address example.com/movies/21.
But I would like to have a profile page of every movie on the nicer URL address, for example example.com/lords-of-the-rings.
How can I do it?
In your model, store the url name into a new field, like Movie.permalink
In config/routes.rb :
MyApp::Application.routes.draw do
match "movies/:permalink" => "movies#show"
end
And in your controller :
class MoviesController < ApplicationController
def show
#movie = Movie.find_by_permalink( params[:permalink] )
end
end
For more on routes in rails : http://guides.rubyonrails.org/routing.html
Consider using the slugged gem: https://github.com/Sutto/slugged
Many folks like this approach.
It's rails 3+
Just to help guide the answers, would you allow something like:
http://example.com/movies/lord-of-the-rings
If so, grabbing the params[:id] from that URL is easy.
You can automate the generation of that last :id by changing to_param of the model:
class User < ActiveRecord::Base
def to_param # overridden
name
end
end
Then you could change the show method of the controller to reflect the new :id format.
user = User.find_by_name(params[:id])