Changing respond_to url for an ActiveModel object - ruby-on-rails

Summary:
How do I customize the path that respond_to generates for an ActiveModel object?
Update: I'm looking for a hook, method override, or configuration change to accomplish this, not a workaround. (A workaround is easy but not elegant.)
Context & Example:
Here is an example to illustrate. I have a model, Contract, which has a lot of fields:
class Contract < ActiveRecord::Base
# cumbersome, too much for a UI form
end
To make the UI code easier to work with, I have a simpler class, SimpleContract:
class SimpleContract
include ActiveModel::Model
# ...
def contract_attributes
# convert SimpleContract attributes to Contract attributes
end
def save
Contract.new(contract_attributes).save
end
end
This works well, but I have a problem in my controller...
class ContractsController < ApplicationController
# ...
def create
#contract = SimpleContract.new(contract_params)
flash[:notice] = "Created Contract." if #contract.save
respond_with(#contract)
end
# ...
end
The problem is that respond_with points to simple_contract_url, but I want it to point to contract_url instead. What is the best way to do that? (Please note that I'm using ActiveModel.)
(Note: I'm using Rails 4 Beta, but that isn't central to my problem. I think a good answer for Rails 3 will work as well.)
Sidebar: if this approach to wrapping a model in a lightweight ActiveModel class seem unwise to you, please let me know in the comments. Personally, I like it because it keeps my original model simple. The 'wrapper' model handles some UI particulars, which are intentionally simplified and give reasonable defaults.

First, here is an answer that works:
class SimpleContract
include ActiveModel::Model
def self.model_name
ActiveModel::Name.new(self, nil, "Contract")
end
end
I adapted this answer from kinopyo's answer to Change input name of model.
Now, for the why. The call stack of respond_to is somewhat involved.
# Start with `respond_with` in `ActionController`. Here is part of it:
def respond_with(*resources, &block)
# ...
(options.delete(:responder) || self.class.responder).call(self, resources, options)
end
# That takes us to `call` in `ActionController:Responder`:
def self.call(*args)
new(*args).respond
end
# Now, to `respond` (still in `ActionController:Responder`):
def respond
method = "to_#{format}"
respond_to?(method) ? send(method) : to_format
end
# Then to `to_html` (still in `ActionController:Responder`):
def to_html
default_render
rescue ActionView::MissingTemplate => e
navigation_behavior(e)
end
# Then to `default_render`:
def default_render
if #default_response
#default_response.call(options)
else
controller.default_render(options)
end
end
And that is as far as I've gotten for the time being. I have not actually found where the URL gets constructed. I know that it happens based on model_name, but I have not yet found the line of code where it happens.

I'm not sure that I fully understand your problem, but could you do something like this?
class SimpleContract
include ActiveModel::Model
attr_accessor :contract
# ...
def contract_attributes
# convert SimpleContract attributes to Contract attributes
end
def save
self.contract = Contract.new(contract_attributes)
contract.save
end
end
-
class ContractsController < ApplicationController
# ...
def create
#simple_contract = SimpleContract.new(contract_params)
flash[:notice] = "Created Contract." if #simple_contract.save
respond_with(#simple_contract.contract)
end
# ...
end
I may be way off base. Hopefully that at least triggers an idea for you.

Related

Run block defined on class within instance's scope

I would like to create something similar to ActiveRecord validation: before_validate do ... end. I am not sure how could I reference attributes of class instance from the block given. Any idea?
class Something
attr_accessor :x
def self.before_validate(&block)
#before_validate_block = block
end
before_validate do
self.x.downcase
end
def validate!
# how should this method look like?
# I would like that block would be able to access instance attributes
end
end
#3limin4t0r's answer covers mimicing the behavior in plain ruby very well. But if your are working in Rails you don't need to reinvent the wheel just because you're not using ActiveRecord.
You can use ActiveModel::Callbacks to define callbacks in any plain old ruby object:
class Something
extend ActiveModel::Callbacks
define_model_callbacks :validate, scope: :name
before_validate do
self.x.downcase
end
def validate!
run_callbacks :validate do
# do validations here
end
end
end
Featurewise it blows the socks off any of the answers you'll get here. It lets define callbacks before, after and around the event and handles multiple callbacks per event.
If validations are what you really are after though you can just include ActiveModel::Validations which gives you all the validations except of course validates_uniqueness_of which is defined by ActiveRecord.
ActiveModel::Model includes all the modules that make up the rails models API and is a good choice if your are declaring a virtual model.
This can be achieved by using instance_eval or instance_exec.
class Something
attr_accessor :x
# You need a way to retrieve the block when working with the
# instance of the class. So I've changed the method so it
# returns the +#before_validate_block+ when no block is given.
# You could also add a new method to do this.
def self.before_validate(&block)
if block
#before_validate_block = block
else
#before_validate_block
end
end
before_validate do
self.x.downcase
end
def validate!
block = self.class.before_validate # retrieve the block
instance_eval(&block) # execute it in instance context
end
end
How about this?
class Something
attr_accessor :x
class << self
attr_reader :before_validate_blocks
def before_validate(&block)
#before_validate_blocks ||= []
#before_validate_blocks << block
end
end
def validate!
blocks = self.class.before_validate_blocks
blocks.each {|b| instance_eval(&b)}
end
end
Something.before_validate do
puts x.downcase
end
Something.before_validate do
puts x.size
end
something = Something.new
something.x = 'FOO'
something.validate! # => "foo\n3\n"
This version allows us to define multiple validations.

Encapsulating controller logic in rails

I have 2 controllers in rails with different authentications schemes,
but they do almost the same.
What is the best way in rails to encapsulate
the logic of a controller in another class or helper?
Sample:
def ControllerA < BasicAuthController
def create
blablacode
end
end
def ControllerB < TokenAuthController
def create
blablacode
end
end
Whats the proper way to do this? create a model with the code?
Create a helper? other?
The simplest thing is to make a module and then include it into the other controllers:
module ControllerMixin
def create
blablacode
end
end
The remaining question, though, is where to put this code such that it is works with Rails autoloader, since it needs to be loaded before the controllers. One way to do it would be to write the module to a file in the lib/ directory, then add that to the autoload paths (see auto-loading-lib-files-in-rails-4
Why don't you enable both schemes for a single controller? Especially if the only difference is Authentication. You could have two app/controllers/concerns to encapsulate both authentication methods and include Auth1 and include Auth2 for a single controller who is only responsible for whatever resource it manages.
Otherwise, services are the best approach to encapsulate controller logic.
Create a folder called services in your app folder and write PORO classes here. Say you have a few places in your app where you want to pay for stuff via make Stripe.
# app/services/stripe_service.rb
module StripeService
def customer(args)
...
end
def pay(amount, customer)
...
end
def reverse(stripe_txn_id)
...
end
end
# controller
StripeService.customer(data)
=> <#Stripe::Customer>
Or if you only need to do one thing.
# app/services/some_thing.rb
module SomeThing
def call
# do stuff
end
end
# controller
SomeThing.call
=> # w/e
If you need an object with multiple reponsibilities you could create a class instead.
class ReportingService
def initialize(args)
...
end
def query
...
end
def data
...
end
def to_json
...
end
end
https://blog.engineyard.com/2014/keeping-your-rails-controllers-dry-with-services
I do it something like this:
#app/services/my_app/services/authentication.rb
class MyApp::Services::Authentication
class < self
def call(params={})
new(params).call
end
end # Class Methods
#==============================================================================================
# Instance Methods
#==============================================================================================
def initialize(params)
#params = params
end
def call
... do a lot of clever stuff
... end by returning true or false
end
private
def params() #params end
end
Then:
class FooController < ApplicationController
before_action :authenticate
def authenticate
redirect_to 'some_path' unless MyApp::Services::Authenticate.call(with: 'some_params')
end
end
Short answer, i choose to create a Helper.
From all the suggestions in the answers
Create a Module:
Seems correct but it didnt feel right to have logic outside
the app directory. This wasnt an external module or library but
something very related to the logic of my app.
Integrate diferents authentications in one controller:
Was a good suggestion but i have to change all the logic of my app.
Create a Helpers:
It seems to me the better solution, i had the code on a helper, and
is inside the app directory, very near from the other logic.

Is there a DRY approach to applying a filter hash in a RAILS API controller index action?

According to the JSON API specification, we should use a filter query parmeter to filter our records in a controller. What the filter parameter actually is isn't really specified, but since it should be able to contain multiple criteria for searching, the obvious thing to do would be to use a hash.
The problem is, it seems like I'm repeating myself quite often in controller actions for different types of records.
Here's what things look like for just a filter that includes a list of ids (to get multiple specific records).
def index
if params[:filter] and params[:filter][:id]
ids = params[:filter][:id].split(",").map(&:to_i)
videos = Video.find(ids)
else
videos = Video.all
end
render json: videos
end
For nested property checks, I guess I could use fetch or andand but it still doesn't look dry enough and I'm still doing the same thing across different controllers.
Is there a way I could make this look better and not repeat myself that much?
Rather than using concerns to just include the same code in multiple places, this seems like a good use for a service object.
class CollectionFilter
def initialize(filters={})
#filters = filters
end
def results
model_class.find(ids)
end
def ids
return [] unless #filters[:id]
#filters[:id].split(",").map(&:to_i)
end
def model_class
raise NotImplementedError
end
end
You could write a generic CollectionFilter as above, then subclass to add functionality for specific use cases.
class VideoFilter < CollectionFilter
def results
super.where(name: name)
end
def name
#filters[:name]
end
def model_class
Video
end
end
You would use this in your controller as below;
def index
videos = VideoFilter.new(params[:filter]).results
render json: videos
end
Here is my take on this, somewhat adapted from Justin Weiss' method:
# app/models/concerns/filterable.rb
module Filterable
extend ActiveSupport::Concern
class_methods do
def filter(params)
return self.all unless params.key? :filter
params[:filter].inject(self) do |query, (attribute, value)|
query.where(attribute.to_sym => value) if value.present?
end
end
end
end
# app/models/user.rb
class User < ActiveRecord::Base
include Filterable
end
# app/controllers/users_controller.rb
class UsersController < ApplicationController
# GET /users
# GET /users?filter[attribute]=value
def index
#users = User.filter(filter_params)
end
private
# Define which attributes can this model be filtered by
def filter_params
params.permit(filter: :username)
end
end
You would then filter the results by issuing a GET /users?filter[username]=joe. This works with no filters (returns User.all) or filters that have no value (they are simply skipped) also.
The filter is there to comply with JSON-API. By having a model concern you keep your code DRY and only include it in whatever models you want to filter. I've also used strong params to enforce some kind protection against "the scary internet".
Of course you can customize this concern and make it support arrays as values for filters.
you can use Rails Concerns to drying up ...
##================add common in app/models/concerns/common.rb
module Common
extend ActiveSupport::Concern
# included do
##add common scopes /validations
# end
##NOTE:add any instance method outside this module
module ClassMethods
def Find_using_filters (params)
Rails.logger.info "calling class method in concern=======#{params}=="
##Do whatever you want with params now
#you can even use switch case in case there are multiple models
end
end
end
##======================include the concern in model
include Common
##=======================in your controller,call it directly
Image.Find_using_filters params

Rails 2.3 - implement dynamic named_scope using mixin

I use the following method_missing implementation to give a certain model an adaptable named_scope filtering:
class Product < ActiveRecord::Base
def self.method_missing(method_id, *args)
# only respond to methods that begin with 'by_'
if method_id.to_s =~ /^(by\_){1}\w*/i
# extract column name from called method
column = method_id.to_s.split('by_').last
# if a valid column, create a dynamic named_scope
# for it. So basically, I can now run
# >>> Product.by_name('jellybeans')
# >>> Product.by_vendor('Cyberdine')
if self.respond_to?( column.to_sym )
self.send(:named_scope, method_id, lambda {|val|
if val.present?
# (this is simplified, I know about ActiveRecord::Base#find_by_..)
{ :conditions => ["#{base.table_name}.#{column} = ?", val]}
else
{}
end
})
else
super(method_id, args)
end
end
end
end
I know this is already provided by ActiveRecord::Base using find_by_<X>, but I'm trying to go a little bit beyond the example I've given and provide some custom filtering taylored to my application. I'd like to make it available to selected models w/o having to paste this snippet in every model class. I thought of using a module and then mixing it in the models of choice - I'm just a bit vague on the syntax.
I've gotten as far as this when the errors started piling up (am I doing this right?):
module GenericFilter
def self.extended(base)
base.send(:method_missing, method_id, *args, lambda { |method_id, args|
# ?..
})
end
end
Then I hope to be able to use it like so:
def Product < ActiveRecord::Base
include GenericFilter
end
def Vendor < ActiveRecord::Base
include GenericFilter
end
# etc..
Any help will be great - thanks.
Two ways of achieving this
module GenericModule
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def methods_missing
#....
end
end
end
class YourModel
include GenericModule
..
end
or
module GenericModule
def method_missing
#...
end
end
class MyModel
extend GenericModule
end
I would suggest using the first one, its seems cleaner to me. And as general advise, I'd avoid overriding method_missing :).
Hope this helps.
You need to define the scope within the context of the class that is including your mixin. Wrap your scopes in including_class.class_eval and self will be correctly set to the including_class.
module Mixin
def self.included(klass)
klass.class_eval do
scope :scope_name, lambda {|*args| ... }
end
end
end
class MyModel
include Mixin
end

How can I disallow updates except for on one field?

I've been preventing updates to certain models by using this in the model:
def update
self.errors.add_to_base( "Cannot update a #{ self.to_s }" )
end
I'm now writing a plugin that delivers some extra functionality to the model, and I need to update one field in the model. If I weren't using a plugin I would do this directly in the model...
def update
if self.changed == ['my_field']
super
else
self.errors.add_to_base( "Cannot update a #{ self.to_s }" )
end
end
I can't do the same from my plugin since I don't know if the update behaviour is the ActiveRecord default, or has been overridden to prevent updates. Is there another way to prevent record updates while allowing me to override for a specific field (and only in the instance where my plugin is applied to this model).
First, you should be using a before_update callback for that sort of thing rather than overriding update. Second, you can store the updatable attributes on the model, and then update them with the plugin. I just wrote this in the browser, so it could be wrong.
attr_accessor :updatable_attributes
before_update :prevent_update
private
def prevent_update
return true if self.changed == self.updatable_attributes
self.errors.add_to_base "Cannot update a #{ self.to_s }"
false
end
end
Late to the game here, but for people viewing this question, you can use attr_readonly to allow writing to a field on create, but not allowing updates.
See http://api.rubyonrails.org/classes/ActiveRecord/ReadonlyAttributes/ClassMethods.html
I think it has been available since Rails 2.0
The tricky part is, if you have any attributes that are attr_accessible you have to list your read only attributes there also (or you get a mass assignment error on create):
class Post < ActiveRecord::Base
attr_readonly :original_title
attr_accessible :latest_title, :original_title
end
Is this to prevent mass assignment? Would attr_accessible / attr_protected not do what you need?
Edit, just to illustrate the general point about the callback.
module MyModule
def MyModule.included(base)
base.send :alias_method_chain, :prevent_update, :exceptions
end
def prevent_update_with_exceptions
end
end
class MyModel < ActiveRecord::Base
before_validation :prevent_update
def prevent_update
end
include MyModule
end
I just use the rails params.require method to whitelist attributes that you want to allow.
def update
if #model.update(update_model_params)
render json: #model, status: :ok
else
render json: #model.errors, status: :unprocessable_entity
end
end
private
def update_prediction_params
params.require(:model).permit(:editable_attribute)
end

Resources