How to avoid saving a blank model which attributes can be blank - ruby-on-rails

I have two models with a HABTM association, let´s say book and author.
class Book
has_and_belongs_to_many :authors
end
class Author
has_and_belongs_to_many :books
end
The author has a set of attributes (e.g. first-name,last-name,age) that can all be blank (see validation).
validates_length_of :first_name, :maximum => 255, :allow_blank => true, :allow_nil => false
In the books_controller, I do the following to append all authors to a book in one step:
#book = Book.new(params[:book])
#book.authors.build(params[:book][:authors].values)
My question: What would be the easiest way to avoid the saving of authors which fields are all blank to prevent too much "noise" in the database?
At the moment, I do the following:
validate :must_have_some_data
def must_have_some_data
empty = true
hash = self.attributes
hash.delete("created_at")
hash.delete("updated_at")
hash.each_value do |value|
empty = false if value.present?
end
if (empty)
errors.add_to_base("Fields do not contain any data.")
end
end
Maybe there is an more elegant, Rails-like way to do that.
Thanks.

A little shorter
def must_have_some_data
hash = self.attributes
hash.delete("created_at")
hash.delete("updated_at")
errors.add_to_base("Fields do not contain any data.") if hash.select{|k,v| !v.blank?}.empty?
end
Actually I think, that you should validate not all attributes, but just specific attributes, which you are expecting to presence
def must_have_some_data
valid_fields = ['first_name', 'second_name', 'last_name']
errors.add_to_base("Fields do not contain any data.") if self.attributes.select{|k,v| valid_fields.include? k and !v.blank?}.empty?
end
UPD
In this situation you should also check authors fields in controller. So your authors fields must be in separate params group.
def create
book = Book.new(params[:book])
params[:authors].each do |author|
book.authors.build(author) unless author.attributes.each{|k,v| !v.blank?}.empty?
end
if book.save
...
end
end

put this in the books model:
validates_associated :authors, :on => :create
Unless you want invalid author objects to be silently ignored but not saved. Then the current solution is one way of solving it.
What version of rails are you using? accepts_nested_attributes_for might be of use in this situation.

You can change one line :)
def create
book = Book.new(params[:book])
params[:authors].each do |author|
# book.authors.build(author) unless author.attributes.each{|k,v| !v.blank?}.empty?
book.authors.build(author) unless author.all? {|key,val| val.empty?}
end
if book.save
...
end
end

Related

Handling join table entries based on association attributes

TL;DR
What is the best way to create join table entries based on a form with the attributes of a association, like a bar code or a plate number?
Detailed explanation
In this system that records movements of items between storage places, there is a has_many_and_belongs_to_many relationship between storage_movements and storage_items because items can be moved multiple times and multiple items can be moved at once.
These items are previously created and are identified by a plate number that is physically attached to the item and recorded on its creation on the application.
The problem is that I need to create storage_movements with a form where the user inputs only the plate number of the storage_item that is being moved but I cant figure it out a way to easily do this.
I have been hitting my head against this wall for some time and the only solution that I can think of is creating nested fields on the new storage_movements form for the storage_items and use specific code on the model to create, update and delete these storage_movements by explicitly querying these plate numbers and manipulating the join table entries for these actions.
Is this the correct way of handling the problem? The main issue with this solution is that I can't seem to display validation errors on the specific plates number that are wrong (I'm using simple_forms) because I don't have storage_item objects to add errors.
Below there is a snipped of the code for the form that I'm currently using. Any help is welcome :D
# views/storage_movements/_form.html.erb
<%= simple_form_for #storage_movement do |movement_form| %>
#Other form inputs
<%= movement_form.simple_fields_for :storage_items do |item_form| %>
<%= item_form.input :plate, label: "Plate number" %>
<% end %>
<% end %>
# models/storage_movement.rb
class StorageMovement < ActiveRecord::Base
has_many_and_belongs_to_many :storage_items, inverse_of: :storage_movements, validate: true
accepts_nested_attributes_for :storage_items, allow_destroy: true
... several callbacks and validations ...
end
# models/storage_item.rb
class StorageItem < ActiveRecord::Base
has_many_and_belongs_to_many :storage_movements, inverse_of: :storage_items
... more callbacks and validations ...
end
The controllers were the default generated ones.
This was my solution, it really "feels" wrong and the validations also are not shown like I want it to... But it was what I could come up with... Hopefully it helps someone.
I created the create_from_plates and update_from_plates methods on the model to handle the create and update and updated the actions of the controller to use them.
Note: had to switch to a has_many through association due to callback necessities.
# models/storage_movement.rb
class StorageMovement < ActiveRecord::Base
has_many :movements_items, dependent: :destroy, inverse_of: :storage_movement
has_many :storage_items, through: :movements_items, inverse_of: :allocations, validate: true
accepts_nested_attributes_for :storage_items, allow_destroy: true
validate :check_plates
def StorageMovement::create_from_plates mov_attributes
attributes = mov_attributes.to_h
items_attributes = attributes.delete "items_attributes"
unless items_attributes.nil?
item_plates = items_attributes.collect {|k, h| h["plate"]}
items = StorageItem.where, plate: item_plates
end
if not items_attributes.nil? and item_plates.length == items.count
new_allocation = Allocation.new attributes
movements_items.each {|i| new_allocation.items << i}
return new_allocation
else
Allocation.new mov_attributes
end
end
def update_from_plates mov_attributes
attributes = mov_attributes.to_h
items_attributes = attributes.delete "items_attributes"
if items_attributes.nil?
self.update mov_attributes
else
transaction do
unless items_attributes.nil?
items_attributes.each do |k, item_attributes|
item = StorageItem.find_by_plate(item_attributes["plate"])
if item.nil?
self.errors.add :base, "The plate #{item_attributes["plate"]} was not found"
raise ActiveRecord::Rollback
elsif item_attributes["_destroy"] == "1" or item_attributes["_destroy"] == "true"
self.movements_items.destroy item
elsif not self.items.include? item
self.movements_items << item
end
end
end
self.update attributes
end
end
end
def check_plates
movements_items.each do |i|
i.errors.add :plate, "Plate not found" if StorageItem.find_by_plate(i.plate).nil?
end
end
... other validations and callbacks ...
end
With this, the create works as I wanted, because, in case of a error, the validation adds the error to the specific item attribute. But the update does not because it has to add the error to the base of the movement, since there is no item.

Rails 4: Assigning a records "has many" attribute without database saving

I have a couple models shown below and I'm using the search class method in Thing to filter records
class Category << ActiveRecord::Base
has_many :thing
end
class Thing << ActiveRecord::Base
belongs_to :category
:scope approved -> { where("approved = true") }
def self.search(query)
search_condition = "%" + query + "%"
approved.where('name LIKE ?', search_condition)
end
end
It works fine in my Things controller. The index route looks like so:
def index
if params[:search].present?
#things = Thing.search(params[:seach])
else
#thing = Thing.all
end
end
On the categories show route I display the Things for this category. I also have the search form to search within the category.
def show
#category = Categories.find(params[:id])
if params[:search].present?
#category.things = #category.things.search()
end
end
So the problem is that the category_id attribute of all the filtered things are getting set to nil when I use the search class method in the categories#show route. Why does it save it to database? I thought I would have to call #category.save or update_attribute for that. I'm still new to rails so I'm sure its something easy I'm overlooking or misread.
My current solution is to move the if statement to the view. But now I'm trying to add pages with kaminiri to it and its getting uglier.
<% if params[:search].present? %>
<% #category.things.search(params[:search]) do |thing| %>
... Show the filtered things!
<% end %>
<% else %>
<% #category.things do |thing| %>
... Show all the things!
<% end %>
<% end %>
The other solution I thought of was using an #things = #categories.things.search(params[:search]) but that means I'm duplicated things passed to the view.
Take a look at Rails guide. A has_many association creates a number of methods on the model to which collection=(objects) also belongs. According to the guide:
The collection= method makes the collection contain only the supplied
objects, by adding and deleting as appropriate.
In your example you are actually assigning all the things found using #category.things.search() to the Category which has previously been queried using Categories.find(params[:id]).
Like Yan said, "In your example you are actually assigning all the things found using #category.things.search() to the Category which has previously been queried using Categories.find(params[:id])". Validations will solve this problem.
Records are being saved as nil because you have no validations on your model. Read about active record validations.
Here's the example they provide. You want to validate presence as well because records are being created without values.
class Person < ActiveRecord::Base
validates :name, presence: true
end
Person.create(name: "John Doe").valid? # => true
Person.create(name: nil).valid? # => false

Rails validations failing due to other side of association not being set

Problem
class Match
has_many :items
end
class Item
belongs_to :match
validates :match, presence: true
end
class ItemBuilder
def self.build
5.times.map { |_| Item.new }
end
end
class MatchBuilder
def self.build
Match.new(items: ItemBuilder.build)
end
end
match = MatchBuilder.build
match.save # Validation fails because Item#match isn't set!
It seems to me that rails should set the Item#match association when assigning Match#items, but this is not the case.
In reality, the ItemBuilder builds Items from information from an API. I'd prefer it not to have knowledge of where those Items are going to be put, so I can't pass the match into ItemBuilder.
I'd also prefer to not have the Matchbuilder aware of the internals of the Items returned from ItemBuilder, so I can't do
class MatchBuilder
def self.build
items = ItemBuilder.build
match = Match.new(items: items)
items.each do |item|
item.match = match
end
end
end
Is there any way of getting around this, without explicitly setting Item#match?
Possible Solutions
Remove the validation, and leave the database to do the not null validation
Assign Item#match in a before_validation filter
I'd do something like this:
match = Match.new
5.times { match.items.build }
match.save
Not sure if autosave: true is needed for items association in this case.

Rails throwing error on save (arguments 1 for 0) when saving through has_many

Due to the nature of my use of 'updated_at' (specifically for use in atom feeds), I need to avoid updating the updated_at field when a record is saved without any changes. To accomplish that I read up and ended up with the following:
module ActiveRecord
class Base
before_validation :clear_empty_strings
# Do not actually save the model if no changes have occurred.
# Specifically this prevents updated_at from being changed
# when the user saves the item without actually doing anything.
# This especially helps when synchronizing models between apps.
def save
if changed?
super
else
class << self
def record_timestamps; false; end
end
super
class << self
remove_method :record_timestamps
end
end
end
# Strips and nils strings when necessary
def clear_empty_strings
attributes.each do |column, value|
if self[column].is_a?(String)
self[column].strip.present? || self[column] = nil
end
end
end
end
end
This works fine on all my models except for my Email model. An Email can have many Outboxes. An outbox is basically a two-column model that holds a subscriber (email To:) and an email (email to send to subscriber). When I update the attributes of an outbox and then save Email, I get the (arguments 1 for 0) error on save (it points to the 'super' call in the save method).
Email.rb
has_many :outboxes, :order => "subscriber_id", :autosave => true
Outbox.rb
belongs_to :email, :inverse_of => :outboxes
belongs_to :subscriber, :inverse_of => :outboxes
validates_presence_of :subscriber_id, :email_id
attr_accessible :subscriber_id, :email_id
UPDATE: I also noticed that the 'changed' array isn't being populated when I change the associated models.
#email.outboxes.each do |out|
logger.info "Was: #{ out.paused }, now: #{ !free }"
out.paused = !free
end unless #email.outboxes.empty?
#email.save # Upon saving, the changed? method returns false...it should be true
...sigh. After spending countless hours trying to find a solution I came across this. Has I known that the 'save' method actually takes an argument I would have figured this out sooner. Apparently looking at the source didn't help in that regard. All I had to do was add an args={} parameter in the save method and pass it to 'super' and everything is working now. Unmodified records are saved without updating the timestamp, modified records are saved with the timestamp and associations are saved without error.
module ActiveRecord
class Base
before_validation :clear_empty_strings
# Do not actually save the model if no changes have occurred.
# Specifically this prevents updated_at from being changed
# when the user saves the item without actually doing anything.
# This especially helps when synchronizing models between apps.
def save(args={})
if changed?
super args
else
class << self
def record_timestamps; false; end
end
super args
class << self
remove_method :record_timestamps
end
end
end
# Strips and nils strings when necessary
def clear_empty_strings
attributes.each do |column, value|
if self[column].is_a?(String)
self[column].strip.present? || self[column] = nil
end
end
end
end

I feel like this needs to be refactored - any help? Ruby modeling

So let's say you have
line_items
and line_items belong to a make and a model
a make has many models and line items
a model belongs to a make
For the bare example idea LineItem.new(:make => "Apple", :model => "Mac Book Pro")
When creating a LinteItem you want a text_field box for a make and a model. Makes and models shouldn't exist more than once.
So I used the following implementation:
before_save :find_or_create_make, :if => Proc.new {|line_item| line_item.make_title.present? }
before_save :find_or_create_model
def find_or_create_make
make = Make.find_or_create_by_title(self.make_title)
self.make = make
end
def find_or_create_model
model = Model.find_or_create_by_title(self.model_title) {|u| u.make = self.make}
self.model = model
end
However using this method means I have to run custom validations instead of a #validates_presence_of :make due to the associations happening off a virtual attribute
validate :require_make_or_make_title, :require_model_or_model_title
def require_make_or_make_title
errors.add_to_base("Must enter a make") unless (self.make || self.make_title)
end
def require_model_or_model_title
errors.add_to_base("Must enter a model") unless (self.model || self.model_title)
end
Meh, this is starting to suck. Now where it really sucks is editing with forms. Considering my form fields are a partial, my edit is rendering the same form as new. This means that :make_title and :model_title are blank on the form.
I'm not really sure what the best way to rectify the immediately above problem is, which was the final turning point on me thinking this needs to be refactored entirely.
If anyone can provide any feedback that would be great.
Thanks!
I don't think line_items should belong to a make, they should only belong to a model. And a model should have many line items. A make could have many line items through a model. You are missing a couple of methods to have your fields appear.
class LineItem
belongs_to :model
after_save :connect_model_and_make
def model_title
self.model.title
end
def model_title=(value)
self.model = Model.find_or_create_by_title(value)
end
def make_title
self.model.make.title
end
def make_title=(value)
#make = Make.find_or_create_by_title(value)
end
def connect_model_and_make
self.model.make = #make
end
end
class Model
has_many :line_items
belongs_to :make
end
class Make
has_many :models
has_many :line_items, :through => :models
end
It's really not that bad, there's just not super easy way to do it. I hope you put an autocomplete on those text fields at some point.

Resources