Prologue
I've embraced Squeel – and enjoying every step! Thank you so much for sharing, Ernie Miller!
I'm developing with ruby 1.9.2 and Squeel 1.0.2 and Rails 3.2.5
(I'll confess to having restructured the question entirely - hoping to increase the readability and better my chances of getting an answer) <:)
Use case
I'd like a (super)user to be able to assign authorizations and permissions like this
a user_group should be able to have multiple authorizations
an authorization should be able to have multiple permissions
a permission should be able to control access to (manipulating) data
via the controller (the request path)
on instances of a Class
on any particular instance
The ACL system should be lazy – ie if no roles/authorizations are given, the users obviously does not concern themselves with ACL at all.
Migrations
I identified role and (a polymorphic) roleable entities from the use case an thus I have
a Role right out of the ordinary
create_table :roles do |t|
t.references :ox
t.string :name
t.boolean :active, default: true
t.timestamps
end
and a Roleable a bit more descriptive
create_table :roleables do |t|
t.references :ox
t.references :role
t.references :roleable, polymorphic: true
t.string :authorization
t.string :controller
t.boolean :active, default: true
t.timestamps
end
Classes
The system has a generic class - AbstractActionBase - which inherits from ActiveRecord:Base, and which all classes inherits from (allowing me to add systemwide attributes and methods in one place)
So - in part - my AbstractActionBase looks like
class AbstractActionBase < ActiveRecord::Base
self.abstract_class=true
require 'tempfile'
belongs_to :ox
has_many :roleables, as: :roleable
attr_accessible :ox_id
validates_presence_of :ox_id
#
# all models inheriting from this will have versions
has_paper_trail
#
#
#
# Class method to providing for index SELECT's being married with roleables (permissions)
# used from abstraction_actions_controller where build_collection calls this method
# the result 'should' be an ActiveRelation - used for the Kamanari 'result' call to readying pagination
#
def self.with_authorizations
#
# SELECT * FROM any_table at
# left join (
# select r.roleable_id, r.roleable_type, group_concat( r.authorization )
# from roleables r
# where r.authorization is not null
# and r.roleable_id=at.id
# and r.roleable_type=at.base_class
# and r.role_id not in (1,2,3) <--- ID's are current_user.roles
# ) rm on rm.roleable_id=at.id and rm.roleable_type=at.base_class
#
# which will provide for this:
#
# |.......| last column in table 'at' | roleable_id | roleable_type | authorizations |
# |.......| some value | 1 | 'UserGroup' | 'insert,create'|
# |.......| yet another value | 92 | 'UserGroup' | 'read' |
#
#
self.where{ active==true }
end
# compile a collection of records - regard search using Ransack
def base.collection( params, resource_set )
#
# kaminari (and continous scrolling)
#
params[:page] ||= 1
params[:per_page] ||= self.per_page
params[:o] ||= self.resource_order_by
distinct = params[:distinct].nil? ? false : params[:distinct].to_i.zero?
resource_set = (resource_set.respond_to?( "result")) ? resource_set.result(:distinct => distinct) : resource_set
(resource_set.respond_to?( "page")) ? resource_set.order(params[:o]).page( params[:page] ).per( params[:per_page] ) : resource_set.order(params[:o])
end
end
Part of the Role class looks like this
class Role < AbstractActionBase
has_many :roleables
scope :active, where{ active.eq true }
#
# what does this role allow
def permissions
roleables.permissions.scoped
end
#
# to whom does this role allow
def authorizations
roleables.authorizations.scoped
end
# returns true if the roleables (permissions) authorizes the options
# options are { controller: "", action: "", record: Instance, is_class: boolean }
def authorizes?( options={} )
coll = permissions
coll = coll.on_action(options.delete(:action)) if options.keys.include? :action
coll = coll.on_entity( options.delete(:record), options.delete(:is_class) || false ) if options.keys.include? :record
coll = coll.on_controller(options.delete(:controller)) if options.keys.include? :controller
(coll.count>0) === true
end
end
The Roleable class looks like this
class Roleable < AbstractActionBase
belongs_to :role
belongs_to :roleable, polymorphic: true
# roleables authorizes users through user_groups
# (in which case the authorization is "-")
# providing them permissions on controllers, actions and instances
scope :authorizations, where{ authorization == nil }
scope :permissions, where{ authorization != nil }
# using Squeel, find roleables on a particular controller or any controller
def self.on_controller(ctrl)
where{ (controller==ctrl) | (controller==nil) }
end
# using Squeel, find roleables on a particular authorization or allowed 'all'
def self.on_action(action)
where{ (authorization=~ "%#{action}%") | (authorization=="all") }
end
# using Squeel, find roleables on a particular instance/record or class
def self.on_entity(entity, is_class=false)
if is_class
where{ ((roleable_type==entity.base_class.to_s ) & ( roleable_id==nil)) | ((roleable_type==nil) & (roleable_id==nil)) }
else
where{ ((roleable_type==entity.class.to_s ) & ( roleable_id==entity.id)) | ((roleable_type==nil) & (roleable_id==nil)) }
end
end
end
Logic
Creating
This allows me authorizations - assigning roles to someone/something - in which case the authorization string is nil, like
The user_group sales is assigned the role sales
with Roleable.create({ role: #sales, roleable: #user_group })
At the same time I can do permissions - describing the particulars of any role - like
The role sales has index, create, edit and delete permissions
on the OrderHead and OrderDetail tables with
Roleable.create({ role: #sales, authorization: "index,create,edit,delete", roleable: #user_group, controller: "order_heads" })
Roleable.create({ role: #sales, authorization: "index,create,edit,delete", roleable: #user_group, controller: "order_details" })
these 'particulars' can be ethereal like
Roleable.create({ role: #sales, authorization: "index" })
somewhat real
Roleable.create({ role: #sales, authorization: "index", roleable_type: 'OrderHead' })
or very expressed
Roleable.create({ role: #sales, authorization: "index", roleable: OrderHead.first })
Selecting
Most every controller inherits from AbstractActionsController where the index (and other actions) are defined. That controller it self inherits from InheritedResources:Base like this
class AbstractActionsController < InheritedResources::Base # < ApplicationController
append_view_path ViewTemplate::Resolver.instance
respond_to :html, :xml, :json, :js, :pdf
belongs_to :ox, :optional => true
before_filter :authorize!
before_filter :authenticate!
before_filter :warn_unless_confirmed!
before_filter :fix_money_params, :only => [:create,:update]
# GET /collection - printers
def index
# session[:params] = params
#
# preparing for Ransack
unless params[:q].nil?
params[:q]= { :"#{params[:q_fields]}" => params[:q] }
end
super do |format|
format.html
format.js { render layout: false }
format.pdf{ render :pdf => generate_pdf(false) and return }
format.xml { render layout: false }
format.json do
# field lookup request?
unless params[:lookup].nil?
render layout: false, :json => collection.map(&:select_mapping)
else
render json: collection.map { |p| view_context.grow_mustache_for_index(p, collection, (parent? ? collection : resource_class.order(:id)), #selected ) }
end
end
end
end
# the collection method on inherited_resources
# gets overloaded with Ransack search and Kaminari pagination (on the model)
def collection
# #collection ||= build_collection
# TODO - test whether caching the collection is possible
build_collection
end
def build_collection
unless params[:belongs].nil?
# debugger
parent = params[:belongs].constantize.find(params[:belongs_id])
#selected = parent.nil? ? [] : parent.send( rewrite_association(params[:assoc],parent) )
#search_resource = core_entity(params[:assoc].constantize)
#search_resource = #search_resource.search(params[:q]) unless params[:q].nil?
else
#search_resource = rewrite_end_of_association_chain(resource_class)
#search_resource = core_entity(#search_resource)
#search_resource = #search_resource.search(params[:q]) unless params[:q].nil?
end
# authorize rows
#search_resource = #search_resource.with_authorizations # left joins roleables coalescing a "authorization" field from roles ID's not owned by current_user through his user_groups
#resources ||= resource_class.collection( params, #search_resource )
end
end
Challenge
What a long story to presenting a short question <:)
How do I write the with_authorizations method to returning a ActiveRelation (and preferably using Squeel)
Walt,
You may be making this more complicated than necessary. If I'm reading this right, the primary purpose of the subquery is to get a concatenated list of the authorizations available in the results. If this is the case, you can simply eager_load authorizations and expose their names via a method on the Role model which does the concatenation for you. This has the secondary upside of being compatible with DBs other than MySQL.
Like I said - preferably using Squeel :)
It turns out that (from the horses mouth so to speak) joins are for associations in Squeel-county ;)
So - what to do? Well, I did one last tour de SO with my SQL-to-ActiveRecord lasso swinging, and lo' and behold! Someone had asked a great question - and there was an even greater answer! Perfect.
In a few short almost fever blinded moments I hacked away using the technique described - and Heureka !!
Previously, I added a pastiebin to aid possible "answerees" - so I've added the result to the pastiebin - but in short it goes like this:
Model.select("something").joins("to your hearts contend")
Cheers,
Walther
Related
I have an attribute encypted using attr_encrypted and I'm using as_json. Under some circumstances I don't want the ssn to be part of a API response, and other times I want it to be included but using the name ssn not encrypted_ssn and to show the decrypted value. In all my cases encrypted_ssn should not be included in the result of as_json.
My first question is, how do I get as_json to return the decrypted ssn field?
With this code
class Person
attr_encrypted :ssn, key: 'key whatever'
end
I want this
Person.first.as_json
=> {"id"=>1,
"ssn"=>"333-22-4444"}
What I don't want is this:
Person.include_ssn.first.as_json
=> {"id"=>1,
"encrypted_ssn"=>"mS+mwRIsMI5Y6AzAcNoOwQ==\n"}
My second question is, how do I make it so a controller using a model can choose to include the decrypted ssn in the JSON ("ssn"=>"333-22-4444") or exclude the field (no "encrypted_ssn"=>"mS+mwRIsMI5Y6AzAcNoOwQ==\n")? I don't even want encrypted values going out to the client if the controller doesn't explicitly specify to include it.
This is what I have so far and seems to work:
class Person
attr_encrypted :ssn, key: 'key whatever'
scope :without_ssn, -> { select( column_names - [ 'encrypted_ssn' ]) }
default_scope { without_ssn }
end
Person.first.as_json
=> {"id"=>1}
I haven't figured out how to make this work in a way that includes the decrypted ssn field as in the first question. What I would like is something like this:
Person.include_ssn.first.as_json
=> {"id"=>1,
"ssn"=>"333-22-4444"}
My final question is, how do I make the above work through a join and how do I specify to include or exclude the encrypted value (or scope) in the join?
With this code:
class Person
has_many :companies
attr_encrypted :ssn, key: 'key whatever'
scope :without_ssn, -> { select( column_names - [ 'encrypted_ssn' ]) }
default_scope { without_ssn }
end
class Company
belongs_to :person
end
This seems to work like I want it
Company.where(... stuff ...).joins(:person).as_json(include: [ :person ])
=> {"id"=>1,
"person"=>
{"id"=>1}}
But I don't know how to implement include_ssn like below or alternatives to tell the person model to include the ssn decrypted.
Company.where(... stuff ...).joins(:person).include_ssn.as_json(include: [ :person ])
=> {"id"=>1,
"person"=>
{"id"=>1,
"ssn"=>"333-22-4444"}}
I've solved this in a different way. Originally I was doing this:
app/models/company.rb
class Company
# ...
def self.special_get_people
people = Company.where( ... ).joins(:person)
# I was doing this in the Company model
people.instance_eval do
def as_json_with_ssn
self.map do |d|
d.as_json(except: [:encrypted_ssn] ).merge('ssn' => d.person.ssn)
end
end
def as_json(*params)
if params.empty?
super(except: [:encrypted_ssn] ).map{ |p| p.merge('ssn' => nil) }
else
super(*params)
end
end
end
return people
end
end
app/controllers/person_controller.rb
class PersonController < ApplicationController
def index
#people = Company.special_get_people
# Then manually responding with JSON
respond_to do |format|
format.html { render nothing: true, status: :not_implemented }
format.json do
render json: #people.as_json and return unless can_view_ssn
render json: #people.as_json_with_ssn
end
end
end
end
However this was fragile and error prone. I've since refactored the above code to look more like this:
app/models/company.rb
class Company
# ...
def self.special_get_people
Company.where( ... ).joins(:person)
end
end
app/controllers/person_controller.rb
class PersonController < ApplicationController
def index
#people = Company.special_get_people
end
end
app/views/person/index.jbuilder
json.people do
json.array!(#people) do |person|
json.extract! person, :id # ...
json.ssn person.ssn if can_view_ssn
end
end
And this ends up being a much better solution that's more flexible, more robust and easier to understand.
For example in my Car model i have such fields:
color, price, year
and in form partial i generate form with all this fields. But how to code such logic:
user could enter color and year and i must find with this conditions, user could enter just year or all fields in same time...
And how to write where condition? I could write something like:
if params[:color].present?
car = Car.where(color: params[:color])
end
if params[:color].present? && params[:year].present?
car = Car.where(color: params[:color], year: params[:year])
end
and so over....
But this is very ugly solution, i'm new to rails, and want to know: how is better to solve my problem?
Check out the has_scope gem: https://github.com/plataformatec/has_scope
It really simplifies a lot of this:
class Graduation < ActiveRecord::Base
scope :featured, -> { where(:featured => true) }
scope :by_degree, -> degree { where(:degree => degree) }
scope :by_period, -> started_at, ended_at { where("started_at = ? AND ended_at = ?", started_at, ended_at) }
end
class GraduationsController < ApplicationController
has_scope :featured, :type => :boolean
has_scope :by_degree
has_scope :by_period, :using => [:started_at, :ended_at], :type => :hash
def index
#graduations = apply_scopes(Graduation).all
end
end
Thats it from the controller side
I would turn those into scopes on your Car model:
scope :by_color, lambda { |color| where(:color => color)}
scope :by_year, lambda { |year| where(:year => year)}
and in your controller you would just conditionally chain them like this:
def index
#cars = Car.all
#cars = #cars.by_color(params[:color]) if params[:color].present?
#cars = #cars.by_year(params[:year]) if params[:year].present?
end
user_params = [:color, :year, :price]
cars = self
user_params.each do |p|
cars = cars.where(p: params[p]) if params[p].present?
end
The typical (naive, but simple) way I would do this is with a generic search method in my model, eg.
class Car < ActiveRecord::Base
# Just pass params directly in
def self.search(params)
# By default we return all cars
cars = all
if params[:color].present?
cars = cars.where(color: params[:color])
end
if params[:price1].present? && params[:price2].present?
cars = cars.where('price between ? and ?', params[:price1], params[:price2])
end
# insert more fields here
cars
end
end
You can easily keep chaining wheres onto the query like this, and Rails will just AND them all together in the SQL. Then you can just call it with Car.search(params).
I think you could use params.permit
my_where_params = params.permit(:color, :price, :year).select {|k,v| v.present?}
car = Car.where(my_where_params)
EDIT: I think this only works in rails 4, not sure what version you're using.
EDIT #2 excerpt from site I linked to:
Using permit won't mind if the permitted attribute is missing
params = ActionController::Parameters.new(username: "john", password: "secret")
params.permit(:username, :password, :foobar)
# => { "username"=>"john", "password"=>"secret"}
as you can see, foobar isn't inside the new hash.
EDIT #3 added select block to where_params as it was pointed out in the comments that empty form fields would trigger an empty element to be created in the params hash.
I need to select available taxons and their childs.
I'm using this custom rule:
module Spree
class Promotion
module Rules
class TaxonPromotionRule < Spree::PromotionRule
has_and_belongs_to_many :taxon, class_name: '::Spree::Taxon', join_table: 'spree_taxons_promotion_rules', foreign_key: 'promotion_rule_id'
validate :only_one_promotion_per_product
MATCH_POLICIES = %w(any all)
preference :match_policy, :string, default: MATCH_POLICIES.first
# scope/association that is used to test eligibility
def eligible_taxons
taxon
end
def applicable?(promotable)
promotable.is_a?(Spree::Order)
end
def eligible?(order, options = {})
return false if eligible_taxons.empty?
if preferred_match_policy == 'all'
eligible_taxons.all? {|p| order.products.include_taxon?(p) }
else
order.products.any? {|p| eligible_taxons.any? {|t| t.include_product?(p)} }
end
end
def taxon_ids_string
taxon_ids.join(',')
end
def taxon_ids_string=(s)
self.taxon_ids = s.to_s.split(',').map(&:strip)
end
private
def only_one_promotion_per_product
if Spree::Promotion::Rules::TaxonPromotionRule.all.map(&:taxon).flatten.uniq!
errors[:base] << "You can't create two promotions for the same product"
end
end
end
end
end
end
and decorator:
Spree::Taxon.class_eval do
def include_product? p
products.include? p
end
end
I want eligible_taxons to be taxon from rules table and all child id. So if I set some root category this rule would apply for all child categories. I hope my question is understandable and clear. :)
Found it. Looks complicated for a newbie (me) on RoR. But here it is:
def eligible_taxons
taxon_with_childs = []
taxon.each { |t| t.self_and_descendants.each{|s| taxon_with_childs << s} }
taxon_with_childs.uniq
end
It builds new list of descendants and self. More details about these functions are here https://github.com/collectiveidea/awesome_nested_set/blob/master/lib/awesome_nested_set/model/relatable.rb
Because after building this list some rows are identical and repeats few times we return only unique taxon_with_childs.uniq
This probably not the best performing algorithm but it does what I needed and fits well with amount of data.
Converting a Rails 2 application to Rails 3, I have to replace the gem searchlogic. Now, using Rails 3.2.8 with the gem Ransack I want to build a search form which uses an existing scope. Example:
class Post < ActiveRecord::Base
scope :year, lambda { |year|
where("posts.date BETWEEN '#{year}-01-01' AND '#{year}-12-31'")
}
end
So far as I know, this can be achieved by defining a custom ransacker. Sadly, I don't find any documentation about this. I tried this in the Postclass:
ransacker :year,
:formatter => proc {|v|
year(v)
}
But this does not work:
Post.ransack(:year_eq => 2012).result.to_sql
=> TypeError: Cannot visit ActiveRecord::Relation
I tried some variations of the ransacker declaration, but none of them work. I Need some help...
UPDATE: The scope above is just on example. I'm looking for a way to use every single existing scope within Ransack. In MetaSearch, the predecessor of Ransack, there is a feature called search_methods for using scopes. Ransack has no support for this out of the box yet.
ransack supports it out of the box after merging https://github.com/activerecord-hackery/ransack/pull/390 . you should declare ransakable_scopes method to add scopes visible for ransack.
From manual
Continuing on from the preceding section, searching by scopes requires defining a whitelist of ransackable_scopes on the model class. The whitelist should be an array of symbols. By default, all class methods (e.g. scopes) are ignored. Scopes will be applied for matching true values, or for given values if the scope accepts a value:
class Employee < ActiveRecord::Base
scope :activated, ->(boolean = true) { where(active: boolean) }
scope :salary_gt, ->(amount) { where('salary > ?', amount) }
# Scopes are just syntactical sugar for class methods, which may also be used:
def self.hired_since(date)
where('start_date >= ?', date)
end
private
def self.ransackable_scopes(auth_object = nil)
if auth_object.try(:admin?)
# allow admin users access to all three methods
%i(activated hired_since salary_gt)
else
# allow other users to search on `activated` and `hired_since` only
%i(activated hired_since)
end
end
end
Employee.ransack({ activated: true, hired_since: '2013-01-01' })
Employee.ransack({ salary_gt: 100_000 }, { auth_object: current_user })
Ransack let's you create custom predicates for this, unfortunately the documentation leaves room for improvement however checkout: https://github.com/ernie/ransack/wiki/Custom-Predicates
Also I believe the problem you're trying to tackle is up on their issue tracker. There's a good discussion going on there: https://github.com/ernie/ransack/issues/34
I wrote a gem called siphon which helps you translate parameters into activerelation scopes. Combining it with ransack can achieves this.
You can read full explanation here. Meanwhile here's the gist of it
The View
= form_for #product_search, url: "/admin/products", method: 'GET' do |f|
= f.label "has_orders"
= f.select :has_orders, [true, false], include_blank: true
-#
-# And the ransack part is right here...
-#
= f.fields_for #product_search.q, as: :q do |ransack|
= ransack.select :category_id_eq, Category.grouped_options
```
ok so now params[:product_search] holds the scopes and params[:product_search][:q] has the ransack goodness. We need to find a way, now, to distribute that data to the form object. So first let ProductSearch swallow it up in the controller:
The Controller
# products_controller.rb
def index
#product_search = ProductSearch.new(params[:product_search])
#products ||= #product_formobject.result.page(params[:page])
end
The Form Object
# product_search.rb
class ProductSearch
include Virtus.model
include ActiveModel::Model
# These are Product.scopes for the siphon part
attribute :has_orders, Boolean
attribute :sort_by, String
# The q attribute is holding the ransack object
attr_accessor :q
def initialize(params = {})
#params = params || {}
super
#q = Product.search( #params.fetch("q") { Hash.new } )
end
# siphon takes self since its the formobject
def siphoned
Siphon::Base.new(Product.scoped).scope( self )
end
# and here we merge everything
def result
Product.scoped.merge(q.result).merge(siphoned)
end
end
My models look like the following:
class Template < ActiveRecord::Base
has_many :template_strings
accepts_nested_attributes_for :template_strings
end
class TemplateString < ActiveRecord::Base
belongs_to :template
end
The TemplateString model is identified by a compound key, on language_id and template_id (it currently has an id primary key as well, but that can be removed if necessary).
Because I am using accepts_nested_attributes_for, I can create new strings at the same time I am creating a new template, which works as it should. However, when I try to update a string in an existing template, accepts_nested_attributes_for tries to create new TemplateString objects, and then the database complains that the unique constraint has been violated (as it should).
Is there any way to get accepts_nested_attributes_for to use a compound key when determining if it should create a new record or load an existing one?
The way I solved this problem was to monkey patch accepts_nested_attributes_for to take in a :key option, and then assign_nested_attributes_for_collection_association and assign_nested_attributes_for_one_to_one_association to check for an existing record based on those key attributes, before continuing on as normal if not found.
module ActiveRecord
module NestedAttributes
class << self
def included_with_key_option(base)
included_without_key_option(base)
base.class_inheritable_accessor :nested_attributes_keys, :instance_writer => false
base.nested_attributes_keys = {}
end
alias_method_chain :included, :key_option
end
module ClassMethods
# Override accepts_nested_attributes_for to allow for :key to be specified
def accepts_nested_attributes_for_with_key_option(*attr_names)
options = attr_names.extract_options!
options.assert_valid_keys(:allow_destroy, :reject_if, :key)
attr_names.each do |association_name|
if reflection = reflect_on_association(association_name)
self.nested_attributes_keys[association_name.to_sym] = [options[:key]].flatten.reject(&:nil?)
else
raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
end
end
# Now that we've set up a class variable based on key, remove it from the options and call
# the overriden method to continue setup
options.delete(:key)
attr_names << options
accepts_nested_attributes_for_without_key_option(*attr_names)
end
alias_method_chain :accepts_nested_attributes_for, :key_option
end
private
# Override to check keys if given
def assign_nested_attributes_for_one_to_one_association(association_name, attributes, allow_destroy)
attributes = attributes.stringify_keys
if !(keys = self.class.nested_attributes_keys[association_name]).empty?
if existing_record = find_record_by_keys(association_name, attributes, keys)
assign_to_or_mark_for_destruction(existing_record, attributes, allow_destroy)
return
end
end
if attributes['id'].blank?
unless reject_new_record?(association_name, attributes)
send("build_#{association_name}", attributes.except(*UNASSIGNABLE_KEYS))
end
elsif (existing_record = send(association_name)) && existing_record.id.to_s == attributes['id'].to_s
assign_to_or_mark_for_destruction(existing_record, attributes, allow_destroy)
end
end
# Override to check keys if given
def assign_nested_attributes_for_collection_association(association_name, attributes_collection, allow_destroy)
unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
end
if attributes_collection.is_a? Hash
attributes_collection = attributes_collection.sort_by { |index, _| index.to_i }.map { |_, attributes| attributes }
end
attributes_collection.each do |attributes|
attributes = attributes.stringify_keys
if !(keys = self.class.nested_attributes_keys[association_name]).empty?
if existing_record = find_record_by_keys(association_name, attributes, keys)
assign_to_or_mark_for_destruction(existing_record, attributes, allow_destroy)
return
end
end
if attributes['id'].blank?
unless reject_new_record?(association_name, attributes)
send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS))
end
elsif existing_record = send(association_name).detect { |record| record.id.to_s == attributes['id'].to_s }
assign_to_or_mark_for_destruction(existing_record, attributes, allow_destroy)
end
end
end
# Find a record that matches the keys
def find_record_by_keys(association_name, attributes, keys)
[send(association_name)].flatten.detect do |record|
keys.inject(true) do |result, key|
# Guess at the foreign key name and fill it if it's not given
attributes[key.to_s] = self.id if attributes[key.to_s].blank? and key = self.class.name.underscore + "_id"
break unless (record.send(key).to_s == attributes[key.to_s].to_s)
true
end
end
end
end
end
Perhaps not the cleanest solution possible, but it works (note that the overrides are based on Rails 2.3).