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
Related
I'm currently implementing liquid templates in my application. As part of that I have created a set of liquid drop (https://github.com/Shopify/liquid/wiki/Trying-to-Understand-Drops) classes to act as intermediates between my models and my templates. I'm currently using devise for authentication on rails 5.
In my product drop class I would like to be able to check if my current user owns the product:
class ProductDrop < Liquid::Drop
def initialize(model)
#model = model
end
def owned_by_user?
#somehow access the current_user provided by devise.
end
end
But haven't been able to figure out how to access the user.
I notice in this method on shopify: https://help.shopify.com/en/themes/liquid/objects/variant#variant-selected
They are able to access the current url to work out if the variant is selected. I thought perhaps it might be possible if they can access the url, to access the session and get the user identifier to look up the user.
So I can do something like:
def owned_by_user?
User.find_by_id(session[:user_id]).owns_product?(#model.id)
end
I'm not having any luck accessing the session. Does anyone have any suggestions or ideas? Or am I going about this completely the wrong way?
So after digging around in the liquid drop source code. I noticed that the context is accessible to the drop (https://github.com/Shopify/liquid/blob/master/lib/liquid/drop.rb). I totally missed it the first time I looked.
So the solution ended up being:
First add the user so it is available to the controller action the view is rendered for. this then gets added to the context by the liquid template handler (and therefore exists in the context)
class ApplicationController < ActionController::Base
before_action :set_common_variables
def set_common_variables
#user = current_user # Or how ever you access your currently logged in user
end
end
Add the method to the product to get the user from the liquid context
class ProductDrop < Liquid::Drop
def initialize(model)
#model = model
end
def name
#model.name
end
def user_owned?
return #context['user'].does_user_own_product?(#model.id)
end
end
Then add the method to the user to check if the user owns the product or not:
class UserDrop < Liquid::Drop
def initialize(model)
#model = model
end
def nick_name
#model.nick_name
end
def does_user_own_product?(id)
#model.products.exists?(id: id)
end
end
Obviously this needs error handling and so on. but hopefully that helps someone. Also if anyone knows of a better way, keen to hear it.
I have a Rails app with a namespaced API and regular controllers that match.
Both sets of controllers of course use the same models. E.g., API::CouponController and CouponController both use the Coupon model.
We are building features into the web side of things first and need to have some conditional logic in the model to tell whether the request is coming from the API or from the regular controller, so we can bifurcate the model logic. How can I do this?
Or is there a way to use a different model for each pair of controllers, but point to the same table?
Try This, I have listed a example it may be helpful
app/controllers/api/coupon_controller.rb
class Api::CouponController < Api::ApiController
def index
Coupon.test_method(params)
end
end
app/controllers/coupon_controller.rb
class CouponController < ApplicationController
def index
Coupon.test_method(params)
end
end
app/models/coupon.rb
class Coupon
def self.test_method(params)
return "You can check your controller and action using params"
end
end
Check The controller and action in test_method
params[:controller]
params[:action]
I added #sort_by attribute to my controller, and initialized it's value like this:
class ProductsController < ApplicationController
def initialize
#sort_by = :shop_brand
end
...
end
This caused the default application layout not to be used.
Why ?
What is the right way to add an attribute to a controller and initialize it ?
Overriding the constructor is probably a bad idea (as you have found). You should use a before_filter:
class ProductsController < ApplicationController
before_filter :set_defaults
...
private
def set_defaults
#sort_by = :shop_brand
end
end
However, it sounds like you want to keep state. The easiest is to store in the user's session which will automatically persist per user until they close the browser:
def set_defaults
session[:sort_by] ||= :shop_brand
end
The other option would be to pass the current sort_by value in the URL. This is harder to implement though as you'll need to ensure each link or form copies the value over to the next request. The advantage of this however is the user could have multiple tabs open with different orderings and any bookmarked link would restore the same ordering next time. This is the approach that things like search engines would use.
I am in the middle of migrating my application from using subdirectories for userspace to subdomains (ie. domain.com/~user to user.domain.com). I've got a method in my user class currently to get the "home" URL for each user:
class User
def home_url
"~#{self.username}"
# How I'd like to do it for subdomains:
#"http://#{self.username}.#{SubdomainFu.host_without_subdomain(request.host)}"
end
end
I'd like to update this for subdomains, but without hardcoding the domain into the method. As you can see, I am using the subdomain-fu plugin, which provides some methods that I could use to do this, except that they need access to request, which is not available to the model.
I know it's considered bad form to make request available in a model, so I'd like to avoid doing that, but I'm not sure if there's a good way to do this. I could pass the domain along every time the model is initialized, I guess, but I don't think this is a good solution, because I'd have to remember to do so every time a class is initialized, which happens often.
The model shouldn't know about the request, you're right. I would do something like this:
# app/models/user.rb
class User
def home_url(domain)
"http://#{username}.#{domain}"
end
end
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# ...
def domain
SubdomainFu.host_without_subdomain(request.host)
end
# Make domain available to all views too
helper_method :domain
end
# where you need it (controller or view)
user.home_url(domain)
If there is such a thing as a canonical user home URL, I would make a configurable default domain (e.g. YourApp.domain) that you can use if you call User#home_url without arguments. This allows you to construct a home URL in places where, conceptually, the "current domain" does not exist.
While molf's answer is good, it did not solve my specific problem as there were some instances where other models needed to call User#home_url, and so there would be a lot of methods I'd have to update in order to pass along the domain.
Instead, I took inspiration from his last paragraph and added a base_domain variable to my app's config class, which is the set in a before_filter in ApplicationController:
module App
class << self
attr_accessor :base_domain
end
end
class ApplicationController < ActionController::Base
before_filter :set_base_domain
def set_base_domain
App.base_domain = SubdomainFu.host_without_subdomain(request.host)
end
end
And thus, when I need to get the domain in a model, I can just use App.base_domain.
I'm working on a multi-user, multi-account App where 1 account can have n users. It is very important that every user can only access info from its account. My approach is to add an account_id to every model in the DB and than add a filter in every controller to only select objects with the current account_id. I will use the authorization plugin.
Is this approach a good idea?
What is the best way to always set the account_id for every object that is created without writing
object.account = #current_account
in every CREATE action? Maybe a filter?
Also I'm not sure about the best way to implement the filter for the select options. I need something like a general condition: No matter what else appears in the SQL statement, there is always a "WHERE account_id = XY".
Thanks for your help!
This is similar to a User.has_many :emails scenario. You don't want the user to see other peoples emails by changing the ID in the URL, so you do this:
#emails = current_user.emails
In your case, you can probably do something like this:
class ApplicationController < ActionController::Base
def current_account
#current_account ||= current_user && current_user.account
end
end
# In an imagined ProjectsController
#projects = current_account.projects
#project = current_account.projects.find(params[:id])
I know, I know, if you access Session-variables or Instance variables in your Model you didn't understand the MVC pattern and "should go back to PHP". But still, this could be very useful if you have - like us - a lot of controllers and actions where you don't always want to write #current_account.object.do_something (not very DRY).
The solution I found is very easy:
Step 1:
Add your current_account to Thread.current, so for example
class ApplicationController < ActionController::Base
before_filter :get_current_account
protected
def get_current_account
# somehow get the current account, depends on your approach
Thread.current[:account] = #account
end
end
Step 2:
Add a current_account method to all your models
#/lib/ar_current_account.rb
ActiveRecord::Base.class_eval do
def self.current_account
Thread.current[:account]
end
end
Step 3: Voilá, in your Models you can do something like this:
class MyModel < ActiveRecord::Base
belongs_to :account
# Set the default values
def initialize(params = nil)
super
self.account_id ||= current_account.id
end
end
You could also work with something like the before_validation callback in active_record and then make with a validation sure the account is always set.
The same approach could be used if you always want to add the current_user to every created object.
What do you think?
To answer your second question, check out the new default_scope feature in Rails 2.3.
I understand that you don't want to bother about scoping you account all time. Lets be honest, it's a pain in the a**.
To add a bit magic and have this scoping done seamlessly give a look at the following gem
http://gemcutter.org/gems/account_scopper
Hope this helps,
--
Sebastien Grosjean - ZenCocoon