Invoice has many invoice entries:
class Invoice < ActiveRecord::Base
has_many :invoice_entries, :autosave => true, :dependent => :destroy
validates_presence_of :date
end
class InvoiceEntry < ActiveRecord::Base
belongs_to :invoice
validates_presence_of :description
end
Assume we have a single invoice in the database:
id: 1
date: '2013-06-16'
and it has two invoice entries:
id: 10 id: 11
invoice_id: 1 invoice_id: 1
description: 'do A' description: 'do C'
Now, I have the new invoice entries:
id: 10
description: 'do B' description: 'do D'
(Existing invoice entry (New invoice entry
with updated description) without id)
I would like the invoice to have only these new invoice entries (this means that invoice entry with id=11 should be deleted).
invoice.invoice_entries = new_invoice_entries seems to do half of the work. It removes the invoice entry with id=11, creates a new invoice entry with description 'Do D', but it doesn't update the description of invoice entry with id=10 from 'Do A' to 'Do B'. I guess that when Rails sees an existing id in new_invoice_entries, it totally ignores it. Is that true? If yes, what is the rationale behind this?
My full code is below. How would you fix this issue? (I use Rails 4, in case it simplifies the code.)
# PATCH/PUT /api/invoices/5
def update
#invoice = Invoice.find(params[:id])
errors = []
# Invoice entries
invoice_entries_params = params[:invoice_entries] || []
invoice_entries = []
for invoice_entry_params in invoice_entries_params
if invoice_entry_params[:id].nil?
invoice_entry = InvoiceEntry.new(invoice_entry_params)
errors << invoice_entry.errors.messages.values if not invoice_entry.valid?
else
invoice_entry = InvoiceEntry.find_by_id(invoice_entry_params[:id])
if invoice_entry.nil?
errors << "Couldn't find invoice entry with id = #{invoice_entry_params[:id]}"
else
invoice_entry.assign_attributes(invoice_entry_params)
errors << invoice_entry.errors.messages.values if not invoice_entry.valid?
end
end
invoice_entries << invoice_entry
end
# Invoice
#invoice.assign_attributes(date: params[:date])
errors << #invoice.errors.messages.values if not #invoice.valid?
if errors.empty?
# Save everything
#invoice.invoice_entries = invoice_entries
#invoice.save
head :no_content
else
render json: errors.flatten, status: :unprocessable_entity
end
end
To change not only the association but also the attributes of the associated objects, you have to use accepts_nested_attributes_for:
class Invoice < ActiveRecord::Base
has_many :invoice_entries, :autosave => true, :dependent => :destroy
validates_presence_of :date
accepts_nested_attributes_for :invoice_entries, allow_destroy: true
end
There's a railscast episode 196 on how to build dynamic nested forms using nested_attributes.
Addendum:
accepts_nested_attributes_for expects attributes for the nested models in a nested hash, i.e.:
invoice_params={"date" => '2013-06-16',
"invoice_entries_attributes" => [
{"description" => "do A"},
{"description" => "do B"}]
}
invoice= Invoice.new(invoice_params)
invoice.save
the save saves invoice and two invoice_items.
Now
invoice=Invoice.find(1)
invoice_params={
"invoice_entries_attributes" => [
{"description" => "do A"},
{"description" => "do C"}]
}
invoice.update_attributes(invoice_params)
deletes the item do B and adds the item do C.
form_fields can be used to create forms that result in exaclty that kind of nested hashes.
For details see the railscast.
Try using accepts_nested_attributes_for. This would clean up a lot of your code! Here is a example:
class Invoice < ActiveRecord::Base
has_many :invoice_entries, :dependent => :destroy
validates_presence_of :date
attr_accessible :invoice_entries_attributes
accepts_nested_attributes_for :invoice_entries, :allow_destroy => true
end
In the view can you then use fields_for (simple_fields_for with simple form, and semantic_fields_for with formtastic if you use one of these gems).
<%= form_for #invoice do |invoice_form| %>
<%= invoice_form.fields_for :invoice_entries do |invoice_entry_form| %>
<%= invoice_entry_form.text_field :description %>
<%= invoice_entry_form.check_box :_destroy %>
<% end %>
<% end %>
In you controller can you now refactor down to the basics:
# PATCH/PUT /api/invoices/5
def update
#invoice = Invoice.find(params[:id])
if #invoice.update_attributes(params[:invoice]) # This also saves all associated invoice entries, and destroy all that is marked for destruction.
head :no_content
else
render json: #invoice.errors.flatten, status: :unprocessable_entity
end
end
You can read more about accepts_nested_attributes_for here: http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
Or you can watch this railscast about nested models: http://railscasts.com/episodes/196-nested-model-form-revised
Related
I have a HABTM relationship using a has_many :through association. I'm having trouble updating the attribute in the join table, because instead of updating the record it just inserts a new record in the table, creating duplicates.
I tried using the UNIQUE constraint when creating the index and adding a validation, but now when I update a record I get a validation error or the error:
ActiveRecord::RecordNotUnique in BatchesController#update
Mysql2::Error: Duplicate entry
To provide some context:
I have 4 tables: manufacturing_orders, order_products, batches and batches_order_products (join table).
A manufacturing_order have many order_products.
Also a manufacturing_order have many batches.
Batches have many order_products.
When I create a batch I copy all the order_products that belongs to the same manufacturing_order, and in the form I can assign a quantity for each of then.
So creating looks like working fine, but when I update any quantity it just inserts the whole relation again instead of updating the existing one.
Model manufacturing_order.rb:
class ManufacturingOrder < ApplicationRecord
has_many :batches, inverse_of: :manufacturing_order, dependent: :destroy
has_many :order_products, inverse_of: :manufacturing_order, dependent: :destroy
accepts_nested_attributes_for :order_products
accepts_nested_attributes_for :batches
end
Model order_product.rb:
class OrderProduct < ApplicationRecord
belongs_to :manufacturing_order
has_many :batches_order_products
has_many :batches, :through => :batches_order_products
end
Model batch.rb:
class Batch < ApplicationRecord
belongs_to :manufacturing_order
has_many :batches_order_products
has_many :order_products, :through => :batches_order_products
accepts_nested_attributes_for :batches_order_products
end
Model batches_order_product.rb:
class BatchesOrderProduct < ApplicationRecord
belongs_to :batch
belongs_to :order_product
validates :batch_id, uniqueness: { scope: :order_product_id }
end
Controller batches_controller.rb:
class BatchesController < ApplicationController
def new
manufacturing_order = ManufacturingOrder.find(params[:manufacturing_order_id])
order_products = manufacturing_order.order_products
#batch = Batch.new({
manufacturing_order: manufacturing_order,
order_products: order_products
})
end
def create
#batch = Batch.new(load_params)
if #batch.save
flash[:notice] = crud_success
redirect_to action: :index
else
flash[:error] = #batch.errors.full_messages.to_sentence
render action: :new
end
end
def edit
#batch = Batch.find(params[:id])
end
def update
#batch = Batch.find(params[:id])
if #batch.update_attributes(load_params)
flash[:notice] = crud_success
redirect_to action: :index
else
flash[:error] = #batch.errors.full_messages.to_sentence
render action: :edit
end
end
private
def load_params
params.require(:batch)
.permit(:name,
:date,
:manufacturing_order_id,
:status,
order_products: [],
order_products_ids: [],
batches_order_products_attributes: [:id, :quantity, :order_product_id]
)
end
end
This is the form in batches:
= bootstrap_form_for([#batch.manufacturing_order, #batch]) do |f|
= f.hidden_field :manufacturing_order_id
= f.text_field :name, label: 'Name'
= f.text_field :date
table
thead
tr
th= "Product"
th= "Quantity"
tbody
= f.fields_for :batches_order_products do |bop|
= bop.hidden_field :order_product_id
tr
td
= bop.object.order_product.name
td
= bop.text_field :quantity
= f.submit 'Save'
Any help will be very much appreciated. Thanks!
UPDATE:
These are the params passed when submitting the edit form. Any clue?
{"utf8"=>"✓",
"_method"=>"patch",
"batch"=>
{"manufacturing_order_id"=>"8",
"name"=>"MAS",
"date"=>"07/05/2020",
"batches_order_products_attributes"=>
{"0"=>{"order_product_id"=>"12", "quantity"=>"77777777", "id"=>""},
"1"=>{"order_product_id"=>"13", "quantity"=>"9.0", "id"=>""},
"2"=>{"order_product_id"=>"14", "quantity"=>"7.0", "id"=>""}}},
"commit"=>"Guardar",
"manufacturing_order_id"=>"8",
"id"=>"7"}
EDIT 2: I updated the nested form to include the id in a hidden field like this:
= f.fields_for :batches_order_products do |bop|
= bop.hidden_field :order_product_id
= bop.hidden_field :id, value: #batch.id
= bop.object.order_product.name
= bop.text_field :quantity, label: ''
BUT now Rails complains of this when updating:
ActiveRecord::StatementInvalid in BatchesController#update
Mysql2::Error: Unknown column 'batches_order_products.' in 'where clause': SELECT `batches_order_products`.* FROM `batches_order_products` WHERE `batches_order_products`.`batch_id` = 9 AND `batches_order_products`.`` IN ('9', '9', '9', '9', '9')
I don't know why Rails adds that last weird part in SQL query.
So I've finally figured it out.
The problem was that the join table needed an ID column to reference to. The table had an index of batch_id and order_product_id but for some reason it didn't work and ActiveRecord was looking for an ID. Adding it solved the problem.
Thanks to #max for giving some points to look at.
class AddIndexToBatchesOrderProductsJoinTable < ActiveRecord::Migration[5.2]
def change
# add_index :batches_order_products, [:batch_id, :order_product_id], unique: true
add_column :batches_order_products, :id, :primary_key
end
end
I am working on a rails app. I have a listings form which creates the listings. There are no validations while creating the listings form other than on some basic fields. But I wont let the users publish the listings if any of the fields are not filled up. In their dashboard I am showing all their listings.
But how can I find the listings with unfilled database fields??..so that I can show it in a different uncompleted listings tab for them to fill out later..The listing model has_many relation with a photos table.
Update as per Rich Peck's answer
For the enum I added a status field to my listing table with default value 0 and its showing published and draft listings as expected on my existing listings. But I cant create new records now..
Getting below error now while updating records
undefined method `draft?' for "draft":String
I think this validation is causing the error
validates :bed_room, :bath_room, :listing_name, :summary, :building_name, presence: true, unless: "status.draft?"
If you wanted to keep your current flow, you'll have to pick through any records in the database which might be nil (difficult):
#Controller
#listings = current_user.listings.unfinished
#app/models/listing.rb
class Listing < ActiveRecord::Base
def self.unfinished
execute(";WITH XMLNAMESPACES('http://www.w3.org/2001/XMLSchema- instance' as ns) SELECT * FROM Listings WHERE (SELECT Listings.*
FOR xml path('row'), elements xsinil, type
).value('count(//*[local-name() != "colToIgnore"]/#ns:nil)', 'int') > 0")
end
end
I have absolutely no idea if the above will work. It uses execute in ActiveRecord to use a pure SQL query.
wont let the users publish the listings
This sounds like you want to make draft functionality.
I would strongly recommend putting validations into your model so that you don't have to pick through a database that might have null values dotted around.
I know you said you have this already; I would make the validations conditional on whether the status of the listing is to be "published" or not (with an enum)...
#app/models/listing.rb
class Listing < ActiveRecord::Base
enum status: [:draft, :published] #-> defaults to draft
belongs_to :user
has_many :photos
scope :draft, -> { where status: :draft }
scope :published, -> { where status: :published }
####
validates :name, :user, :photos, :etc, :etc, presence: true, unless: "status.draft?"
####
def publish!
self.update status: :published
end
end
This would work similarly to the Wordpress "draft" functionality (IE publishing and saving are two completely different things):
#config/routes.rb
resources :listings do
post :publish, on: :member #-> url.com/listings/:id/publish
end
#app/controllers/listings_controller.rb
class ListingsController < ApplicationController
def new
#listing = current_user.listings.new
end
def create
#listing = current_user.listings.new listing_params
#listing.save #-> will be "draft" by default so no validations
end
def publish
#listing = current_user.listings.find params[:id]
redirect_to #listing if #listing.publish!
end
end
In your front-end, you'll be able to then list the #listings by whether they're published or draft:
#app/views/listings/index.html.erb
<% #listings.published do |published| %>
...
<% end %>
<% #listings.draft do |draft| %>
...
<% end %>
Update
According to the OP, the answer was to use the following in his model:
#app/models/listing.rb
class Listing < ActiveRecord::Base
enum status: [:draft, :published] #-> defaults to draft
belongs_to :user
has_many :photos
####
validates :name, :user, :photos, :etc, :etc, presence: true, unless: "draft?"
####
def publish!
self.update status: :published
end
end
Assuming Listing is your model name and required_field is a unfilled field name,
Listing.where(required_field: nil).where(user: current_user)
will give the listings of the current user with unfilled fields.
listings.includes(:photos).where(photos: { id: nil })
I have a Rails 4 application and am having problems creating a new record for a has_many :through association. A couple of observations that I made while debugging:
Commenting out the checkboxes associated with the Features model, the application will create and save the venue object properly.
The update action in the venues controller works fine for the feature checkboxes.
Can somebody tell me why my application is having problems saving the associated features object (an array) when creating a new venue? Not sure if this is because the foreign key, venue_id, doesn't exist prior to the save...
Here's the code:
venues.rb
class Venue < ActiveRecord::Base
has_many :venue_features, dependent: :destroy
has_many :features, :through => :venue_features
venue_features.rb
class VenueFeature < ActiveRecord::Base
belongs_to :venue
belongs_to :feature
features.rb
class Feature < ActiveRecord::Base
has_many :venue_features, dependent: :destroy
has_many :venues, :through => :venue_features
venues\new.html.erb (Features are listed as checkboxes - use selects relevant checkboxes)
<%= hidden_field_tag "venue[feature_ids][]", nil %>
<% Feature.all.each do |feature| %>
<div class="checkbox">
<label>
<%= check_box_tag "venue[feature_ids][]", feature.id, #venue.feature_ids.include?(feature.id) %>
<%= feature.name %><br>
</label>
</div>
<% end %>
venues_controller.rb
class VenuesController < ApplicationController
def create
#venue = Venue.new(venue_params)
if #venue.save(venue_params)
flash[:success] = "Success!"
redirect_to venues_path
else
flash[:error] = "Problem!"
render 'new'
end
end
def venue_params
params.require(:venue).permit(:attribute_1, :attribute_2, :feature_type_ids => [])
end
end
I'm sure there's a cleaner solution but what I ended up doing was to update a different set of strong parameters after a successful save. The problem prob has something to do with 1) one of my parameters, :feature_type_ids, is actually an array and/or 2) :feature_type_ids is in a different model (not the venues.rb). I thought that Rails would "automagically" handle saving to differnet models since venues.rb and features.rb have been set up through a :has_many :through relationship. I'd be curious for a better solution but the following works:
class VenuesController < ApplicationController
def create
...
if #venue.save(venue_only_params)
if #venue.update_attributes(venue_params)
flash[:success] = "Success!"
redirect_to venues_path
else
flash[:error] = "Problem!"
render 'new'
end
end
def venue_params
params.require(:venue).permit(:attribute_1, :attribute_2, :feature_type_ids => [])
end
def venue_only_params
params.require(:venue).permit(:attribute_1, :attribute_2)
end
end
This is in continuation to the question which was raised here
How to add one-to-many objects to the parent object using ActiveRecord
class Foo < ActiveRecord::Base
has_many :foo_bars
end
class Bar < ActiveRecord::Base
end
class FooBar < ActiveRecord::Base
belongs_to :foo
belongs_to :bar
end
How to handle removal of entries in a multi select check box are used to represent one-to-many entities. I am able to add or update entries, but removal seems to fail since foo_id seems to be empty and the query seems to be updating instead of delete.
EDIT :
I tried with #charlysisto suggestion using the following code
My Controller code is as follows :
class Foo < ActiveRecord::Base
has_many :foo_bars
has_many :bars, :through => :foo_bars
end
def edit
#foo = Foo.find(params[:id])
#sites = Site.where(company_id: #current_user.company_id).all
end
def update
#foo = Foo.find(params[:id])
if #foo.update_attributes(params[:foo])
flash[:notice] = "Foo was successfully updated"
redirect_to foos_path
else
render :action => 'edit'
end
end
View code is as follows :
<% #bars.each do |bar| %>
<%= check_box_tag 'bar_ids[]', bar.id %>
<%= bar.name %>
<% end %>
So I tried with these changes, but still foo_bars doesn't seem to reflect the changes if I removed a record.
What's missing in your associations is this :
class Foo < ActiveRecord::Base
has_many :bars, :through => :foo_bars
end
... the has_many (or has_many :through) macro gives you a truck load of methods including bar_ids and bar_ids= [...]
So all you need to do in your views/controller is :
# edit.html.haml which will send an array of bar_ids
=f.select :bar_ids, Bar.all.map {|b| [b.name, b.id]}, {}, :multiple => true
# foo_controller
#foo.update_attributes(params[:foo])
And that's it! No need to manually set or delete the associations in FooController#update...
G'day guys,
I'm currently flitting through building a test "Auction" website to learn rails. I've set up my Auction and User models and have it so that only authenticated users can edit or delete auctions that are associated with them.
What I'm having difficulty doing is associating bid items with the Auction.
My models are as follows:
class Auction < ActiveRecord::Base
belongs_to :creator, :class_name => "User"
has_many :bids
validates_presence_of :title
validates_presence_of :description
validates_presence_of :curprice
validates_presence_of :finish_time
attr_reader :bids
def initialize
#bids = []
end
def add_bid(bid)
#bids << bid
end
end
class Bid < ActiveRecord::Base
belongs_to :auction, :class_name => "Auction", :foreign_key => "auction_id"
belongs_to :bidder, :class_name => "User", :foreign_key => "bidder_id"
validates_presence_of :amount
validates_numericality_of :amount
#retracted = false
end
class User < ActiveRecord::Base
has_many :auctions, :foreign_key => "owner_id"
has_many :bids, :foreign_key => "owner_id"
#auth stuff here
end
I'm attempting to add a bid record to an auction, but the auction_id simply will not add to the record.
I create a bid with a value from within a view of the auction, having the #auction as the local variable.
<% form_for :bid, :url => {:controller => "auction", :action => "add_bids"} do |f|%>
<p>Bid Amount <%= f.text_field :amount %></p>
<%= submit_tag "Add Bid", :auction_id => #auction %>
<% end %>
This is connected to the following code:
def add_bids
#bid = current_user.bids.create(params[:bid])
if #bid.save
flash[:notice] = "New Bid Added"
redirect_to :action => "view_auction", :id => #bid.auction_id
end
end
The problem I am getting is that the auction_id is not put into the bid element. I've tried setting it in the form HTML, but I think I'm missing something very simple.
My Data model, to recap is
Users have both bids and auctions
Auctions have a user and have many bids
Bids have a user and have a auction
I've been struggling with trying to fix this for the past 4 hours and I'm starting to get really downhearted about it all.
Any help would be really appreciated!
You're not quite doing things the Rails way, and that's causing you a bit of confusion.
Successful coding in Rails is all about convention over configuration. Meaning, Rails will guess at what you mean unless you tell it otherwise. There's usually a couple of things it will try if it guesses wrong. But in general stick to the deterministic names and you'll be fine.
There are so many errors in your code, so I'm going to clean it up and put comments every way to let you know what's wrong.
app/models/auction.rb
class Auction < ActiveRecord::Base
belongs_to :creator, :class_name => "User"
has_many :bids
# Given the nature of your relationships, you're going to want to add this
# to quickly find out who bid on an object.
has_many :bidders, :through => :bids
validates_presence_of :title
validates_presence_of :description
validates_presence_of :curprice
validates_presence_of :finish_time attr_reader :bids
#These two methods are unnecessary.
# Also don't override initialize in ActiveRecord. Instead use after_initialize
#def initialize Supplied by rails when you do has_many :bids
# #bids = [] #bids will be populated by what is picked up from
#end the database based on the has_many relationship
#def add_bid(bid) Supplied by rails when you do has_many :bids
# #bids << bid auction.bids << is a public method after has_many :bids
#end
end
app/models/bid.rb
class Bid < ActiveRecord::Base
# :class_name and :foreign_key are ony necessary when rails cannot guess from a
# association name. :class_name default is the association singularized and
# capitalized. :foreign_key default is association_id
belongs_to :auction #, :class_name => "Auction", :foreign_key => "auction_id"
# here we need :class_name because Rails is looking for a Bidder class.
# also there's an inconsistency. Later user refers to has_many bids with
# a foreign_key of owner_id, which one is it? bidder_id or owner_id?
# if it's owner_id? you will need the :foreign_key option.
belongs_to :bidder, :class_name => "User" #, :foreign_key => "bidder_id"
validates_presence_of :amount
validates_numericality_of :amount
# This will never get called in a useful way.
# It really should be done in the migration, setting default
# value for bids.retracted to false
# #retracted = false
end
app/models/user.rb
class User < ActiveRecord::Base
# This makes sense, because an auction can have many bidders, who are also users.
has_many :auctions, :foreign_key => "owner_id"
# This doesn't. A bid belongs to a user, there's no need to change the name.
# See above note re: owner_id vs. bidder_id
has_many :bids, :foreign_key => "owner_id"
# You could also use this to quickly get a list of auctions a user has bid on
has_many :bid_on_auctions, :through => :bids, :source => :auction
... auth stuff ...
end
So far so good, right?
The view isn't bad but it's missing the form parts for the bid amount. This code assumes that you store the value of the bid in an amount column. I also arbitrarily named it auctions/bid
app/views/auctions/bid.html.erb
<% form_for :bid, #auction.bids.new do |f|%>
<%= f.label_for :amount %>
<%= f.text_field :amount%>
<!-- Don't need to supply #auction.id, because form_for does it for you. -->
<%= submit_tag "Add Bid" %>
params hash generated by the form: that is passed to the controller:
params =
{
:bid =>
{
:auction_id => #auction.id
:amount => value of text_field
}
}
params hash generated by the from as you wrote it (note: I'm guessing at names because they were left out of the posted code):
params =
{
:id => #auction_id ,
:bid => { :amount => value of text_field }
}
However, your controller code is where all your problems are coming from this is almost entirely wrong. I'm guessing this is in the auction controller, which seems wrong because you're trying to create a bid. Lets see why:
app/controllers/auctions_controller.rb
...
def add_bids
# not bad, but... #bid will only fill in the owner_id/bidder_id. and bid amount.
#bid = current_user.bids.create(params[:bid])
# create calls save, so this next line is redundant. It still works though.
# because nothing's happening between them to alter the outcome of save.
if #bid.save
flash[:notice] = "New Bid Added"
# you should be using restful routes, this almost works, but is ugly and deprecated.
# it doesn't work becasue #bid.auction_id is never set. In fact you never use
# the auction_id any where, which was in your params_hash as params[:id]
redirect_to :action => "view_auction", :id => #bid.auction_id
end
end
...
Here's how your controller should work. First of all, this should be in the bids_controller, not auctions_controller
app/controllers/bids_controller.rb
...
def create
#bid = Bid.new(params[:bid]) # absorb values from form via params
#bid.bidder = current_user # link bid to current_user.
#auction = bid.auction based on.
# #auction is set, set because we added it to the #bid object the form was based on.
if #bid.save
flash[:notice] = "New Bid Added"
redirect_to #auction #assumes there is a show method in auctions_controller
else
render "auctions/show" # or what ever you called the above view
end
end
...
You'll also need to make sure the following is in your routes.rb (in addition to what may already be there. These few lines will set you up with RESTful routes.
config/routes.rb
ActionController::Routing::Routes.draw do |map|
...
map.resources :auctions
map.resources :bids
...
end
In any case you weren't far off. It seems you're off to a decent start, and could probably benefit from reading a book about rails. Just blindly following tutorials doesn't do you much good if you don't understand the underlying framework. It doesn't help that 90% of the tutorials out there are for older versions of rails and way out of date.
A lot of your code is the old way of doing things. Particularly redirect_to :action => "view_auction", :id => #bid.auction_id and <% form_for :bid, :url => {:controller => "auction", :action => "add_bids"} do |f|%>. With RESTful routing, they become redirect_to #auction and <% form_for #auction.bid.new do |f| %>`
Here's something resources you should read up on:
ActiveRecord::Associations: defines has_many, belongs_to, their options, and the convenience methods they add.
Understanding MVC: Provides a better understanding of the flow of information as it relates to Rails
RESTful resources: Understanding resources.
Form Helpers: In depth description of form_for and why the above code works.