Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails 4.2 strong params and parsing changed attributes on a json put / update

Background

I have several models with about 40 attributes each. They are normalized, it's just that the models we are dealing with have to have multiple attributes. Other than that it's a pretty standard rails 4.2 app ( upgrading from rails 3.2) . The app is used both to serve dynamic pages sprinkled with ajax calls, and json so it can be used by various json clients.

So a call to : http://example.com/products/1.json - returns json and http://example.com/products/1 returns haml-parsed view.

Problem

JavaScript library I'm using ( KendoUI) returns a whole record on update, not just fields that got updated. Currently there is no way to avoid unless I want to rewrite KendoUi Grid according to their support forum.

So user can search all the products, and I display all the attributes of the product to her, and then based on her access level she can update certain fields (several pricing points and description), however ajax request contains ALL the attributes. So as a result I get ActiveModel::ForbiddenAttributesError

Question

Part 1

How can I properly ( the rails way so to speak) filter out params that got updated? Right now I'm doing a hash comparison of @product.attributes && params[:product] to figure out if there are updated/ new attributes passed. .diff was deprecated as of Rails 4.0.2

Part 2

My admins are allowed to change almost all attributes on these big models ( except timestamps, id, and a few others). What is the best way to do it rather than doing params[:product].require(:base_cost).permit(:vendor_cost, :client_cost) for 30 odd attributes? It quickly becomes a problem to maintain these lists if the app is in development and attributes change. I guess I could use some kind of CONSTANT - ALLOWED_ATTRIBUTES or ADMIN_ATTRIBUTES and USER_ATTRIBUTES, and pass that to permit. But kind of feels un-Railsy?

like image 273
konung Avatar asked Nov 11 '22 00:11

konung


1 Answers

Part 1

Even if the function is deprecated you can still use this functionality. With what your trying to do a few might be useful here.

class Hash
  def diff h2
    self.dup.delete_if { |k, v| h2[k] == v }.merge(h2.dup.delete_if { |k, v| self.has_key?(k) })
  end

  def only *args
    h = {}
    args.each do |a|
      h[a] = self[a]
    end
    h
  end

  def except *args
    h = {}
    (self.keys - args).each do |a|
      h[a] = self[a]
    end
    h
  end
end

Tests

2.0.0-p247 :001 > h1 = {a: 1, b: 2, c: 3} #=> {:a=>1, :b=>2, :c=>3} 
2.0.0-p247 :002 > h2 = {a: 3, b: 2, c: 1} #=> {:a=>3, :b=>2, :c=>1} 
2.0.0-p247 :003 > h1.except(:a) #=> {:b=>2, :c=>3} 
2.0.0-p247 :004 > h1.except(:c) #=> {:a=>1, :b=>2} 
2.0.0-p247 :005 > h1.except(:c, :a) #=> {:b=>2} 
2.0.0-p247 :006 > h1.only(:c, :a) #=> {:c=>3, :a=>1}

One thing to keep in mind is the params may not truly come in as a hash, so you may have to call .to_hash

Part 2

class FooBar < ActiveRecord::Base
  #this controls access restriction, assuming you keep this setup:
  #delete this part and all fields are update-able (leave it undefined)
  #no fields can be updated if UPDATABLE_FIELDS[type].nil? or UPDATABLE_FIELDS[type][0].nil?
  UPDATABLE_FIELDS = {
    admin: [:except, :id, :timestamp], 
    user: [:only, :name]
  }
end

class ActiveRecord::Base
  def fields_for_user_type type
    return true unless defined?(UPDATABLE_FIELDS)
    return nil if !UPDATABLE_FIELDS[type] || !UPDATABLE_FIELDS[type].first
    return UPDATABLE_FIELDS[type].first, UPDATABLE_FIELDS[type][1..-1]
  end
end

class ApplicationController < ActionController::Base
  def filter_params data, cls
    method, restriction = cls.fields_for_user_type(current_user.type)
    return data if method === true
    return {} if method.nil?
    return data.except(restriction) if method == :except
    return data.only(restriction) if method == :only
  end
end

class FunController < ApplicationController
  def update
    record = FooBar.find(params[:id])
    record.update(filter_params(params[:data], FooBar))
  end
end

You could add defaults to this pretty easily and some other nifty pieces of functionality but at least as a start this should do it for you.

Please keep in mind not all this has been thoroughly tested, there could be bugs!

like image 62
Camway Avatar answered Nov 14 '22 21:11

Camway