Can accepts_nested_attributes_for update based on a compound key? - ruby-on-rails

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).

Related

Can I use the _destroy attribute in a non-nested form?

Say I have something like this in my controller:
FacultyMembership.update(params[:faculty_memberships].keys,
params[:faculty_memberships].values)
and whenever the _destroy key in params[:faculty_memberships].values is true, the record is destroyed.
Is there something like this in rails? I realize there are other ways of doing this, I was just curious if something like this existed.
Short answer
no!
Long answer
Still no! It is true that it works on nested attributes:
If you want to destroy the associated model through the attributes
hash, you have to enable it first using the :allow_destroy option.
Now, when you add the _destroy key to the attributes hash, with a
value that evaluates to true, you will destroy the associated model.
But why not trying it out in the console:
?> bundle exec rails c
?> m = MyModel.create attr_1: "some_value", attr_2: "some_value"
?> m.update(_destroy: '1') # or _destroy: true
?> ActiveRecord::UnknownAttributeError: unknown attribute '_destroy' for MyModel
This is because the update implementation is the following:
# File activerecord/lib/active_record/persistence.rb, line 245
def update(attributes)
# The following transaction covers any possible database side-effects of the
# attributes assignment. For example, setting the IDs of a child collection.
with_transaction_returning_status do
assign_attributes(attributes)
save
end
end
and the source for assign_attributes is:
# File activerecord/lib/active_record/attribute_assignment.rb, line 23
def assign_attributes(new_attributes)
if !new_attributes.respond_to?(:stringify_keys)
raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
end
return if new_attributes.blank?
attributes = new_attributes.stringify_keys
multi_parameter_attributes = []
nested_parameter_attributes = []
attributes = sanitize_for_mass_assignment(attributes)
attributes.each do |k, v|
if k.include?("(")
multi_parameter_attributes << [ k, v ]
elsif v.is_a?(Hash)
nested_parameter_attributes << [ k, v ]
else
_assign_attribute(k, v)
end
end
assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty?
assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty?
end
This code worked for me:
class FacultyMembership < ApplicationRecord
attr_accessor :_destroy
def _destroy= value
self.destroy if value.present?
end
end
Possibly this can break nested forms with destroy - didn't check.

Rails checking model to see if all fields are empty

As far as I've tested it, this helper method works exactly as it's meant to, however I want to know if there is any easier, built-in, or smarter way to run this check! I also am aware that having this in the ApplicationHelper probably isn't ideal. Not sure if I should just put it in the parent object (the Inspection), some other model, or leave as is.
With is_model_empty? I need to run through every field of any one of eleven different (but similar) models to check to see if all of them are Empty. All of them except the :id, :inspection_id, :created_at, and :updated_at fields which will never be blank. Empty can be nil, can be [], or can be ['']. An empty string would actually imply that the user entered something so that won't be included. The value can be either a string or an array so .empty? won't work.
def is_model_empty?(model)
model.attributes.each do |k, v|
unless ['id', 'inspection_id', 'created_at', 'updated_at'].include?(k)
return false unless v.nil? || v == [] || v == [""]
end
end
true
end
The eleven models all belong to the Inspection and each has a has_one relationship:
has_one :first_info_section
has_one :second_info_section
has_one :third_info_section
Any advice/feedback would be much appreciated. Thanks for reading!
-Dave
Your method can be simplifed as an instance method on each of the models. If the attribute exceptions are the same for all the models you can create a shared library and include it each of the models.
app/models/empty_detection.rb:
module EmptyDetection
def empty?
attributes.all? do |k, v|
['id', 'inspection_id', 'created_at', 'updated_at'].include?(k) || v.nil? || v == [] || v == [""]
end
end
end
Include that module in each model you want to be able to check for the empty conditions. For example, the Widget model:
class Widget < ActiveRecord::Base
include EmptyDetection
end
Now you can use it on any instance of a Widget:
widget = Widget.find(45)
widget.empty?
Here's a really basic refactor:
def is_empty?(model)
whitelist = %w[ id inspection_id created_at updated_at ]
model.attributes.all? do |attr, val|
whitelist.exclude?(attr) || val.nil? || val == [] || val == [""]
end
end
What you really want, though, is a validator, which is described in the Active Record Validations Rails Guide:
In this case it would look like this:
class EmptyValidator < ActiveModel::Validator
WHITELIST = %w[ id inspection_id created_at updated_at ].freeze
def validate(record)
return unless model.attributes.all? do |attr, val|
WHITELIST.exclude?(attr) || empty?(val)
end
record.errors[:base] << "You missed one!"
end
private
def empty?(val)
val.nil? || val == [] || val == [""]
end
end
Then, in each of your models...
class MyModel < ActiveRecord::Base
validates_with EmptyValidator
end
I hope that's helpful!
user.attributes.values.all?(&:blank?)

How can I know which columns in my table are considered unique?

I have two columns in my table which are unique: username and email. I'm trying to create a method which will automatically check if whatever the user inputs is unique or not. It's something like
def check_if_taken(arg = {})
params.each do |key, value|
if (key is a unique column???)
return true if User.where(key => value).present?
end
end
false
end
How do I figure out the unique columns in the table?
Edit: refactored
def self.is_taken?(params = {})
params.detect do |key, value|
User.where(key => value).present?
end
end
You can try sth like:
class ActiveRecord::Base
def self.unique_attribute?(attribute_name)
!!_validators[attribute_name] && !!_validators[attribute_name].find {|v| v.is_a? ActiveRecord::Validations::UniquenessValidator)
end
end
And then:
YourModel.unique_attribute?(attribute_to_check)

Finding all by Polymorphic Type in Rails?

Is there a way to find all Polymorphic models of a specific polymorphic type in Rails? So if I have Group, Event, and Project all with a declaration like:
has_many :assignments, :as => :assignable
Can I do something like:
Assignable.all
...or
BuiltInRailsPolymorphicHelper.all("assignable")
That would be nice.
Edit:
... such that Assignable.all returns [Event, Group, Product] (array of classes)
There is no direct method for this. I wrote this monkey patch for ActiveRecord::Base.
This will work for any class.
class ActiveRecord::Base
def self.all_polymorphic_types(name)
#poly_hash ||= {}.tap do |hash|
Dir.glob(File.join(Rails.root, "app", "models", "**", "*.rb")).each do |file|
klass = File.basename(file, ".rb").camelize.constantize rescue nil
next unless klass.ancestors.include?(ActiveRecord::Base)
klass.
reflect_on_all_associations(:has_many).
select{ |r| r.options[:as] }.
each do |reflection|
(hash[reflection.options[:as]] ||= []) << klass
end
end
end
#poly_hash[name.to_sym]
end
end
Now you can do the following:
Assignable.all_polymorphic_types(:assignable).map(&:to_s)
# returns ['Project', 'Event', 'Group']
You can also try this way.cause above solution doesn't work for me cause i had some mongo's model.
def get_has_many_associations_for_model(associations_name, polymorphic=nil)
associations_name = associations_name.to_s.parameterize.underscore.pluralize.to_sym
active_models = ActiveRecord::Base.descendants
get_model = []
active_models.each do |model|
has_many_associations =model.reflect_on_all_associations(:has_many).select{|a|a.name==associations_name }
has_many_associations = has_many_associations.select{ |a| a.options[:as] == polymorphic.to_s.to_sym} if polymorphic.present?
get_model << model if has_many_associations.present?
end
get_model.map{|a| a.to_s}
end
Anb call it like
get_has_many_associations_for_model("assignments", "assignable")
Here Second parameters is optional for if you want polymorphic records than pass it otherwise leave it as blank.
It will Return Array of Model name as String.
Harish Shetty's solution will not work for namespaced model files which are not stored directly in Rails.root/app/models but in a subdirectory. Although it correctly globs files in subdirectories, it then fails to include the subdir when turning the file name into a constant. The reason for this is, that the namespacing subdir is removed by this line:
klass = File.basename(file, ".rb").camelize.constantize rescue nil
Here is what I did to retain the namespacing subdir:
file.sub!(File.join(Rails.root, "app", "models"), '')
file.sub!('.rb', '')
klass = file.classify.constantize rescue nil
Here's the full modified solution:
def self.all_polymorphic_types(name)
#poly_hash ||= {}.tap do |hash|
Dir.glob(File.join(Rails.root, "app", "models", "**", "*.rb")).each do |file|
file.sub!(File.join(Rails.root, "app", "models"), '')
file.sub!('.rb', '')
klass = file.classify.constantize rescue nil
next unless klass.ancestors.include?(ActiveRecord::Base)
klass.
reflect_on_all_associations(:has_many).
select{ |r| r.options[:as] }.
each do |reflection|
(hash[reflection.options[:as]] ||= []) << klass
end
end
end
#poly_hash[name.to_sym]
end
Now, the method will turn /models/test/tensile.rb correctly into Test::Tensile before reflecting on its associations.
Just a minor improvement, all credit still goes to Harish!
I created a polymorphic model class with a method 'all' to test this.
class Profile
# Return all profile instances
# For class return use 'ret << i' instead of 'ret << i.all'
def self.all
ret = []
subclasses_of(ActiveRecord::Base).each do |i|
unless i.reflect_on_all_associations.select{|j| j.options[:as] == :profile}.empty?
ret << i
end
end
ret.flatten
end
def self.all_associated
User.all.map{|u| u.profile }.flatten
end
end
Here is my app setup:
User < ActiveRecord::Base
belongs_to :profile, :polymorphic => true
end
Student < ActiveRecord::Base
has_one :user, :as => :profile
end
You should be able to just use the associated collection:
model.assignments.all

Is it possible for rails to save multiple data type in same db column?

I tried to simulate variable_set and variable_get in drupal which use as site-wide variables storage. I tried something like this.
# == Schema Information
# Schema version: 20091212170012
#
# Table name: variables
#
# id :integer not null, primary key
# name :string(255)
# value :text
# created_at :datetime
# updated_at :datetime
#
class Variable < ActiveRecord::Base
serialize :value
validates_uniqueness_of :name
validates_presence_of :name, :value
def self.set(name, value)
v = Variable.new()
v.name = name
v.value = value
v.save
end
def self.get(name)
Variable.find_by_name(name).value
end
end
but it doesn't work.
I've found a way to do this using yaml for storing your values as encoded strings.
Since I'm not storing the "values" on the database, but their converstions to strings, I named the column encoded_value instead of value.
value will be a "decoder-getter" method, transforming the yaml values to their correct types.
class Variable < ActiveRecord::Base
validates_uniqueness_of :name
validates_presence_of :name, :encoded_value #change in name
def self.set(name, value)
v = Variable.find_or_create_by_name(name) #this allows updates. name is set.
v.encoded_value = value.to_yaml #transform into yaml
v.save
end
def self.get(name)
Variable.find_by_name(name).value
end
def value() #new method
return YAML.parse(self.encoded_value).transform
end
end
This should return integers, dates, datetimes, etc correctly (not only raw strings). In addition, it should support Arrays and Hashes, as well as any other instances that correctly define to_yaml.
I have the following in one of my applications :
class Configure < ActiveRecord::Base
def self.get(name)
value = self.find_by_key name
return value.value unless value.nil?
return ''
end
def self.set(name, value)
elem= self.find_by_key name
if elem.nil?
#We add a new element
elem = Configure.new
elem.key = name
elem.value = value
elem.save!
else
#We update the element
elem.update_attribute(:value, value)
end
return elem.value
end
end
Which is apparently what you're looking for.

Resources