Rails 4: how to customize route key for a model? - ruby-on-rails

I have a CourseQuestion model in my app.
I use redirect_to #discussible in my controllers, where #discussible could be of different class (therefore redirect is to a different URL).
But I need to redirect CourseQuestion models to question_path, not course_question_path which is default.
I don't need to change routes (routes are fine), just need rails to deduce specific named path for a model.
Any good way to do that?

Something I've done, but somehow feels like a hack is to override the model_name method of the model.
In your case, you could do:
class CourseQuestion
def self.model_name
ActiveModel::Name.new(self, nil, "Question")
end
# if your course_question belongs_to :question, to prevent some unwanted bugs, where course_questions/25 would map to questions/25, add a to_param method that returns the id of the question, as such
def to_param
question_id.to_s # Be sure to stringify the id for routes
end
end

A questionable module to include in models, changing only #route_key and #singular_route_key methods:
module DemodulizedRouteKeys
extend ActiveSupport::Concern
class_methods do
def model_name
## this would demodulize all namings:
# ActiveModel::Name.new self, nil, super.name.demodulize
super.tap do |name|
route_key = name.name.demodulize.underscore
name.define_singleton_method(:route_key) { route_key.pluralize }
name.define_singleton_method(:singular_route_key) { route_key }
end
end
end
end

Related

Rails N+1 query : monkeypatching ActiveRecord::Relation#as_json

Situation
I have a model User:
def User
has_many :cars
def cars_count
cars.count
end
def as_json options = {}
super options.merge(methods: [:cars_count])
end
end
Problem
When I need to render to json a collection of users, I end up being exposed to the N+1 query problem. It is my understanding that including cars doesn't solve the problem for me.
Attempted Fix
What I would like to do is add a method to User:
def User
...
def self.as_json options = {}
cars_counts = Car.group(:user_id).count
self.map do |user|
user.define_singleton_method(:cars_count) do
cars_counts[user.id]
end
user.as_json options
end
end
end
That way all cars counts would be queried in a single query.
Remaining Issue
ActiveRecord::Relation already has a as_json method and therefore doesn't pick the class defined one. How can I make ActiveRecord::Relation use the as_json method from the class when it is defined? Is there a better way to do this?
Edits
1. Caching
I can cache my cars_count method:
def cars_count
Rails.cache.fetch("#{cache_key}/cars_count") do
cars.count
end
end
This is nice once the cache is warm, but if a lot of users are updated at the same time, it can cause request timeouts because a lot of queries have to be updated in a single request.
2. Dedicated method
Instead of calling my method as_json, I can call it my_dedicated_as_json_method and each time I need to render a collection of users, instead of
render json: users
write
render json: users.my_dedicated_as_json_method
However, I don't like this way of doing. I may forget to call this method somewhere, someone else might forget to call it, and I'm losing clarity of the code. Monkey patching seems a better route for these reasons.
Have you considered using a counter_cache for cars_count? It's a good fit for what you're wanting to do.
This blog article also offers up some other alternatives, e.g. if you want to manually build a hash.
If you really wanted to continue down the monkey patching route, then ensure that you are patching ActiveRecord::Relation rather than User, and override the instance method rather than creating a class method. Note that this will then affect every ActiveRecord::Relation, but you can use #klass to add a condition that only runs your logic for User
# Just an illustrative example - don't actually monkey patch this way
# use `ActiveSupport::Concern` instead and include the extension
class ActiveRecord::Relation
def as_json(options = nil)
puts #klass
end
end
Option 1
In your user model:
def get_cars_count
self.cars.count
end
And in your controller:
User.all.as_json(method: :get_cars_count)
Option 2
You can create a method which will get all the users and their car count. And then you can call the as_json method on that.
It would roughly look like:
#In Users Model:
def self.users_with_cars
User.left_outer_joins(:cars).group(users: {:id, :name}).select('users.id, users.name, COUNT(cars.id) as cars_count')
# OR may be something like this
User.all(:joins => :cars, :select => "users.*, count(cars.id) as cars_count", :group => "users.id")
end
And in your controller you can call as_json:
User.users_with_cars.as_json
Here is my solution in case someone else is interested.
# config/application.rb
config.autoload_paths += %W(#{config.root}/lib)
# config/initializers/core_extensions.rb
require 'core_extensions/active_record/relation/serialization'
ActiveRecord::Relation.include CoreExtensions::ActiveRecord::Relation::Serialization
# lib/core_extensions/active_record/relation/serialization.rb
require 'active_support/concern'
module CoreExtensions
module ActiveRecord
module Relation
module Serialization
extend ActiveSupport::Concern
included do
old_as_json = instance_method(:as_json)
define_method(:as_json) do |options = {}|
if #klass.respond_to? :collection_as_json
scoping do
#klass.collection_as_json options
end
else
old_as_json.bind(self).(options)
end
end
end
end
end
end
end
# app/models/user.rb
def User
...
def self.collection_as_json options = {}
cars_counts = Car.group(:user_id).count
self.map do |user|
user.define_singleton_method(:cars_count) do
cars_counts[user.id]
end
user.as_json options
end
end
end
Thanks #gwcodes for pointing me at ActiveSupport::Concern.

How to modify (encode and decode) routes id, but without changes in the model or controller?

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

Creating custom reusable method in rails 4

Guys today I'm trying to create global method for all my project models in rails 4
I created something like that under this path lib/query.rb
module Query
def custom my_query
self.where(my_query)
end
end
then added this code in this file lib/application.rb to allow rails to load the files under this path
# Custom directories with classes and modules you want to be autoloadable.
config.autoload_paths += %W(#{config.root}/lib)
then included my method in my model by using this command
include Query
now should every thing ready to use my custom method , but when I tried to call my method in the controller like that
def index
#users= Users.custom(params[:query])
end
I got the error
undefined method `custom'
what I should do now ??
why i got this error ??
I think you should use concern for your module. Add your file in app/models/concerns.
# app/models/concerns/query.rb
module Query
extend ActiveSupport::Concern
included do
#you can use a scope
scope :my_query, ->(just_a_param){ .... }
end
module ClassMethods
#or a method
def self.another_query
where(....)
end
end
end
Of course you need to include the module in your model. As concern erd default in rails, you no longer need to change config autoload paths.
As a class method, you'll need the "self."
def self.custom my_query
self.where(my_query)
end
EDIT: If you want this in all ActiveRecord models, you can add it as an initializer
#config/initializers/active_record_extensions.rb
class ActiveRecord::Base
def self.custom my_query
self.where(my_query)
end
end
If you just want this on a single class, a concern would work.
In your example, there is no reference given between your class Users and your method custom. First: if Users refers to a Ruby on Rails class it is probably called User (see also comment of japed). So change the call. Next, your User class must inherited from ActiveRecord else it would not be aware of the existence of 'where'. For details check your app/models/user.rb
Then Swards' suggestion should work for you. Stop your application and restart. Now it should work.
Guys I found the true way to make it
First my impropriety was the include that I set in the model
It should be extend Query
then it will work well
so the true code will be
create your method file under this path lib/query.rb
then set this code in it
module Query
def custom my_query
self.where(my_query)
end
end
then added this code in this file lib/application.rb
# Custom directories with classes and modules you want to be autoloadable.
config.autoload_paths += %W(#{config.root}/lib)
then extend the method in the model by using this command
extend Query
and in your controller query you can use the method like that
def index
#users= Users.custom(params[:query])
end
This is my solution, not exactly the 'Rails way', but using some sort of decorator pattern:
#user = CustomQuery.find_for(User.find(params[:search])).perform!
class CustomQuery
attr_reader :params, :klass
def initialize(klass)
#params = params
#klass = klass
end
def self.find_for(params)
CustomQuery.new(params)
find_model_for(params.tap {})
end
def perform!
return params unless params.nil?
klass.all
end
def find_model_for(klass)
#klass = klass
end
end
While I'm not sure about the process to create a global method, I can tell that your Ruby code is not valid:
def custom my_query
self.where(my_query)
end
It would need to be:
def custom (my_query)
self.where(my_query)
end

showing only attributes that are not null in the Rails 3 as_json method

I'm using the as_json method heavily in a couple of models I have in a project I'm working on, and what I'm trying to do is to display those attributes on the fly ONLY if they are not nil/Null ... does anyone have any idea how to go about this?
You can override as_json:
# clean_to_json.rb
module CleanToJson
def as_json(options = nil)
super(options).tap do |json|
json.delete_if{|k,v| v.nil?}.as_json unless options.try(:delete, :null)
end
end
end
# foo.rb
class Foo < ActiveRecord::Base
include CleanToJson
end
Usage:
#foo.as_json # Only present attributes
#foo.as_json(:null => true) # All attributes (former behavior)

Rails: Adding methods to models to perform checks based on current_user?

I have a model that looks something like this:
class Comment < ActiveRecord::Base
...
#allow editing comment if it is moderated and the user passed-in
#is the one that owns the comment
def can_edit?(user)
moderated? and user.Type == User and user.id == self.user_id
end
...
end
And a call in a view:
<%= link_to 'Show Comment', #comment if #comment.can_show?(current_user) %>
I need to write many such methods in many different models - sort of validation checks to see if current_user is allowed to
do something on a model.
But it feels cumbersome - especially the need to check that the passed-in user is indeed a object of type User.
What's a clean, best-practice way to do this sort of thing? Am I on the right track? (i.e. should I be adding such methods to a model or somewhere else)
Note
I am using scoped queries to get the comments and other models, but in some cases I cannot scope the query so I have to use the can_xxxx? methods
Ps. Is what I'm doing considered a "fat model"?
Create a module containing all the authorization methods and include the module to all the classes requiring authorization.
Add a file called authorization.rb to app/models directory.
module Authorization
def can_edit?(user)
moderated? and user.is_a?(User) and user.id == self.user_id
end
def self.included(base)
base.send(:extend, ClassMethods)
end
module ClassMethods
# add your class methods here.
end
end
Add a file called authorization.rb to config/initializers directory.
%w(
Comment
Post
).each do |klass|
klass.constantize.include(Authorization)
end
Now Comment and Post models will have all the authorization methods.
Other approach is to use your current named_scope.
class Post
named_scope :accessible, lambda { |user|
{
:conditions => { :user_id => user.id, :moderated => true}
}
}
end
Post controller actions
class PostsController
def index
#posts = Post.acessible(current_user)
# process data
end
def show
# throws record not found when the record is not accessible.
#post = Post.acessible(current_user).find(params[:id])
# process data
end
end
I like this approach as it uses the same logic for accessing an array of objects or a single object.
You can add the named_scope to the module to avoid repeated definitions:
module Authorization
def self.included(base)
base.named_scope :accessible, lambda { |user|
{
:conditions => { :user_id => user.id, :moderated => true}
}
}
end
module ClassMethods
# add your class methods here.
end
end
Make sure to include the module in required classes as suggested earlier.
I don't think what you're doing is necessarily wrong. I see three ways to simplify, though:
1) track self.user as well as self.user_id. Then you can say:
def can_show?(user)
moderated ? and user == self.user
end
Note, this might add overhead either with DB lookup times and/or memory footprint.
2) Use #is_a? in order to check ancestry and not just class equality:
def can_show?(user)
moderated ? and user.is_a?( User ) and user.id == self.user_id
end
3) If passing in a non-user is wrong, you might want to raise an error when this happens:
def can_show?(user)
raise "expected User, not #{ user.class.to_s }" unless user.is_a?(User)
moderated ? and user.id == self.user_id
end
As for Q2, I haven't heard the terminology "fat model." Is it referenced anywhere in particular?
Re: fat model and skinny controller
This is the idea of pushing logic into the model rather than having it in the controller (or worse, the view).
A big benefit is to help with testing; also the focus of placing more logic in the model rather than in the controller. Remember that it is not uncommon to have controllers work with multiple models.
Putting the logic into a model rather than a controller often means that the business rules are being baked into the model--which is exactly where they belong.
A possible downside is that any information available to the controller that is not available in the model needs to be explicitly passed into the model's methods or "set" using a model's instance variables.
Your example of needing to pass the current user into the model illustrates the issue.
Overall though, I and many others have found that fat models tend to work out better than not.

Resources