I have a ruby (on rails) class:
class User < ActiveRecord::Base
# relationships
belongs_to :current_shipping_address, :class_name => "Address"
belongs_to :subscription
# Validators
validates_presence_of :subscription
validates_presence_of :current_shipping_address
end
I do this in a controller:
subscription = Subscription.new
address_info = params[:user].delete(:address) rescue {}
#address = Address.new(address_info.merge(:country => "US"))
#user = User.new(params[:user].merge(:first_name => #address.first_name, :last_name => #address.last_name))
#user.subscription = subscription
#user.current_shipping_address = #address
#user.save!
At this point, incredibly, I have a #user that has been saved in the database but has no current_shipping_address (despite the validation). The subscription has also been saved in the database.
The address does NOT get saved.
What am I missing here?
1 - how does user get saved without the validation failing?
2 - why is the address not saved?
How can I alter this code so that the address gets saved (as I would expect it to)?
I am running this under ruby on rails 3.
Thanks!
You cannot have subscription and current_shipping_address saved by user in your case because, they are not simple fields in model User. You define them as model associated to User through belongs_to, I'm not sure about what you are willing to do, but if I understand correctly one way to do it is using nested attributes:
class User < ActiveRecord::Base
# relationships
has_many :current_shipping_addresses, :class_name => "Address", :dependant => destroy
has_many :subscriptions, :dependant => destroy
# Nesting
accepts_nested_attributes_for :subscriptions
accepts_nested_attributes_for :current_shipping_addresses
end
After that, when you then create and save a User, a subscription and current_shipping_address are saved whith it .
More on assocations here : http://guides.rubyonrails.org/association_basics.html
You need to tell it what the foreign key is if you're not sticking with the standard table structure. You'll just need to add:
belongs_to :current_shipping_address, :class_name => "Address", :foreign_key => "address_id"
or whatever column you are using to store the address id to the address table.
This is not the recommended way of doing nested attributes though. I would recommend using a fields_for in your form rather than using the lines:
address_info = params[:user].delete(:address) rescue {}
#address = Address.new(address_info.merge(:country => "US"))
You can just do
<%= f.fields_for :current_shipping_address do |ff| %>
# ... your address fields...
<% end %>
which will then let you simply save the address when you run #user.save!
You can still add the :country => "US" beforehand with
params[:user][:current_shipping_address][:country] = "US"
and then run save. Its really up to you though.
Try this way!
subscription = Subscription.new
address_info = params[:user].delete(:address) rescue {}
#user = User.new(params[:user].merge(:first_name => #address.first_name, :last_name => #address.last_name))
#user.subscription = subscription
#user.current_shipping_address << Address.new(address_info.merge(:country => "US"))
#user.save!
Seems the problem was that address was actually failing to save. not because of a validation but because of an error in a 'before_create' method (and yes I know I didn't give you the address object... I didn't think it important at the time!).
class Address < ActiveRecord::Base
# relationships
# Validators
validates_presence_of :city, :state, :country, :first_name, :last_name, :address_1
before_create :check_state
before_create :check_country
def check_state
retval = true
state.upcase!
if country == "US" and !US_STATES.map{|s| s[1]}.include?(state)
errors.add(:state, "Must be valid")
retval = false
end
retval
end
end
Check state was failing. But that meant that address passed the 'valid?' call, which it seems is all active record cares about. (This method really should be a validation)
I have switched to doing this (thanks enokd for the link!):
#user = User.new(params[:user].merge(:first_name => #address.first_name, :last_name => #address.last_name))
#user.build_subscription(:subscription_plan_id => #subscription_plan.id)
#user.build_current_shipping_address(address_info.merge(:country => "US"))
I haven't bothered to investigate fully, but, if address fails to save it stops the whole #user.save!
Personally I think this is a little bit of bug perhaps or certainly an unexpected behaviour, but what do I know!
Try:
class User < ActiveRecord::Base
# relationships
has_one :current_shipping_address, :class_name => "Address", :dependant => destroy
has_many :subscriptions, :dependant => destroy
validates :current_shipping_address, :presence => true
end
Related
I've inherited quite a weird table layout:
callbacks
id, note, user
admin
id, name, password
In callbacks, the user is set to the name of the admin rather than the actual ID. Now I need to be able to call callbacks.user and have rails lookup the admin with that name and then bind it to that record.
I have a model for admin that is called users
How would I go about that?
You can override the default methods.
def user
User.find_by_name(user_name)
end
def user=(obj)
self.user_name = obj.name
end
def user_name
self[:user]
end
def user_name=(name)
self[:user] = name
end
Other option , to make it work with belongs_to, there is primary_key option but need to have a different name than the attribute user
# Callback.rb
belongs_to :user_model , :class => "User", :foreign_key => :user, :primary_key => :name
# User.rb
has_one :callback , :foreign_key => :user, :primary_key => :name
I'm trying to create an address form with shipping and billing address on same page.
When user gets ready for checkout , I want both shipping address form and billing address for to appear on same page. If billing address same as shipping address only record should be inserted into address table , if different two records has to be inserted and of course an update has to take place in orders table shipping_address_id,billing_address_id.
Having only one address model, how do I achieve two forms with one submit button.
Below is my model for address and orders
I need some help in putting in controller also I'm trying to get a hash value for each billing and shipping
Please help!!!
class Address < ActiveRecord::Base
attr_accessible :name,:first_name,:last_name,:address1,:address2,:city,:state,:zip,:phone,:billing_default,: user_id,:billing_address, :shipping_address
belongs_to :user
has_many :billing_addresses, :class_name => "Order", :foreign_key => "billing_address_id"
has_many :shipping_addresses, :class_name => "Order", :foreign_key => "shipping_address_id"
class Order < ActiveRecord::Base
attr_accessible :cart_id, :order_no, :sales_tax, :shipping_fee,:total,:order_state,:gateway_type,:transaction_id,:transaction_status,:ip_address,:card_verification,:card_number,:billing_address_id,:shippin g_address_id,:first_name,:last_name,:user_id,:card_expires_on,:authenticity_token
belongs_to :cart
belongs_to :user
belongs_to :billing_address, :class_name => "Address"
belongs_to :shipping_address, :class_name => "Address"
attr_accessor :card_number
has_many :transactions, :through => :order_id
has_many :invoices
has_many :order_details
This is a slightly complicated problem, you will find.
First, ask yourself: Do you really only want to insert one address if billing and shipping addresses are the same?
A customer wants to change the shipping address. You will need logic to create another address record and retain the original as billing.
Generally, avoid updates to billing and shipping addresses after an order is complete as they compromise data integrity. Once an order is closed, that's it; those addresses should be fixed. When an order requires a different shipping address, avoid having a dependency between it and the billing address.
Now, assuming you're going ahead.
Using Nested Forms
Hide billing fields, and add a check box to your form that maps to an order.bill_to_shipping_address. Default it to checked. Show billing address if it gets unchecked.
$('input[name="order[bill_to_shipping_address]"]').on 'click', ->
if $(this).is ':checked'
$('fieldset.billing_fields').hide()
else
$('fieldset.billing_fields').show()
In your order model:
accepts_nested_attributes_for :shipping_address
accepts_nested_attributes_for :billing_address, reject_if: :bill_to_shipping_address
The draw back with this approach is, if there is a validation error, and the user happens to change his mind and bill to a different address, the billing form will not appear since it gets rejected.
Use a Form Object
This might seem more complex, but it's a much cleaner solution.
See 7 Patterns for refactoring ActiveRecord Objects.
Build a form object as such. I've adopted this code from something I recently wrote for a Rails 4 app. Just reverse your relationships. In my case an order has one billing address and one shipping address; it does not belong to them.
class OrderForm
include ActiveModel::Model
def self.model_name
ActiveModel::Name.new(self, nil, "Order")
end
def persisted?
false
end
attr_accessor :params
delegate :email, :bill_to_shipping_address, to: :order
# Removed most fields for brevity
delegate :name, :street, :street_number, to: :shipping_address, prefix: :shipping
delegate :name, :street, :street_number, to: :billing_address, prefix: :billing
# Removed most fields for brevity
validates :email, length: { maximum: 60 }, email_format: true
validates :shipping_name, :shipping_street, presence: true
validates :billing_name, presence: true, unless: -> { bill_to_shipping_address }
def initialize(params = nil)
#params = params
end
def submit
populate
if valid?
order.save!
true
else
false
end
end
def order
#order ||= Order.new
end
private
def shipping_address
#shipping_address ||= order.build_shipping_address
end
def billing_address
#billing_address ||= order.build_billing_address
end
def populate
order.email = params[:email]
order.bill_to_shipping_address = params[:bill_to_shipping_address]
shipping_address.name = params[:shipping_name]
# etc...
unless order.bill_to_shipping_address?
billing_address.name = params[:billing_name]
# etc...
end
end
end
Then from the controller:
def new
#order_form = OrderForm.new
end
def create
#order_form = OrderForm.new(params[:order])
if #order_form.submit
# order saved, do whatever
else
render 'new'
end
end
Your form now does not care about nested attributes and properties. It's nice a clean.
= form_for #order do |f|
= f.text_field :email
= f.text_field :shipping_street
= f.text_field :billing_street
# etc...
I'd suggest using a checkbox so the user can specify whether use the same billing and shipping address or type different ones.
In the form file you need to handle nested forms in the following way:
= form_for #order do f
= f.fields_for :billing_address do |ba|
= ba.text_field :address1
= ba.text_field:address2
= ba.text_field :city
= ba.text_field :state
= ba.text_field :zip
= ba.text_field :phone
= f.fields_for :shipping_address do |sa|
= sa.text_field :address1
= sa.text_field:address2
= sa.text_field :city
= sa.text_field :state
= sa.text_field :zip
= sa.text_field :phone
In your model don't forget to add:
accepts_nested_attributes_for :shipping_address
accepts_nested_attributes_for :billing_address
And probably need to add the autobuild to your address relations
belongs_to :billing_address, :class_name => "Address", autobuild: true
belongs_to :shipping_address, :class_name => "Address", autobuild: true
In the controller create/update actions, you just need to check the checkbox value and assign them equal, here's one approach:
#order.shipping_address = #order.billing_address if params[:checkbox_use_same_address] == true
Given the following models:
class Company
include Mongoid::Document
has_many :workers, autosave: true
accepts_nested_attributes_for :workers
attr_accessible :workers_attributes
end
class Worker
include Mongoid::Document
field :hours
attr_accessible :hours
belongs_to :company
end
class Manager < Worker
field :order
has_many :contributors, :class_name => "Worker"
attr_accessible :order, :contributors
end
class Contributor < Worker
field :task
belongs_to :manager, :class_name => "Worker"
attr_accessible :task
end
How does one create a manager in a company in the controller and view using nested attributes?
Here's my guess:
def new
#company = Company.new
#company.workers = [Manager.new]
end
def create
#company = Company.new params[:user]
if #company.save
redirect_to root_url, :notice => "Company with manager created."
else
render :new
end
end
= semantic_form_for #company do |f|
= f.semantic_fields_for :workers do |worker_fields|
= worker_fields.inputs do
= worker_fields.input :hours
= worker_fields.input :order
problem is the order field which specifically belongs to the manager is not persisting after the create. Also when the data is improperly filled there is an error:
undefined method `order' for #<Worker:0x0000000646f018> (ActionView::Template::Error)
So is there a way for nested attributes to handle inheritance in the models from mongoid?
The question is related to Can nested attributes be used in combination with inheritance? except instead of active record using mongoid.
Honestly, this is a paraphrasing of my code... the real code is more complex situation although i believe these are all of the relevant parts. If you have more questions ask.
UPDATE:
I changed the view to the following:
= semantic_form_for #company do |f|
- #company.workers.each do |worker|
- if worker._type == "Manager"
= f.semantic_fields_for :workers, worker do |worker_fields|
= worker_fields.inputs do
= worker_fields.input :hours
= worker_fields.input :order
I do not get the error anymore, however the nested attributes do not update the company object properly. The params are the following:
{"company"=> {"workers_attributes"=>{"0"=>{"hours"=>"30", "order" => "fish", "id"=>"4e8aa6851d41c87a63000060"}}}}
Again edited for brevity. So the key part is that there is a hash between "0" => {data for manager}. The workers data seems to be held in a hash. I would expect the data to look more like the following:
params = { company => {
workers_attributes => [
{ hours => "30", "order" => "fish" }
]}}
This is different because the workers data is held in an array instead of a hash. Is there another step to get the nested attributes to save properly?
Thanks
what version of Mongoid are you using? Because I don't think the use of refereneces_many is encouraged -- Not that that's related to your problem here, just wanted to probe what version you're using. In the doc on the gorgeous Mongoid.org, get this, I had to learn it the hard way, they say for Updating your records, you need to the autossave set to true. That's NOT accurate. You need it for even creating
so:
class Company
include Mongoid::Document
has_many :workers, :autossave => true # your money shot
accepts_nested_attributes_for :workers
attr_accessible :workers_attributes
end
ADDED:
I was re-reading your code, I spotted the following that might be the problem: Your Company model is set to has_many :workers and is set to accept nested attribbutes for Worker when changes come in, correct? And there is a field named Order in your Manager model which is subclassed from Worker. Yet you're having a form whose nested fields part is pointed at Worker not at Manager, the model that actually has the Order field. And that's obviously not enough, because Company isn't having_many :managers yet, you may need to set it to has_many :managers in the Company model as well.
I have a Project model which accepts nested attributes for Task.
class Project < ActiveRecord::Base
has_many :tasks
accepts_nested_attributes_for :tasks, :allow_destroy => :true
end
class Task < ActiveRecord::Base
validates_uniqueness_of :name
end
Uniqueness validation in Task model gives problem while updating Project.
In edit of project i delete a task T1 and then add a new task with same name T1, uniqueness validation restricts the saving of Project.
params hash look something like
task_attributes => { {"id" =>
"1","name" => "T1", "_destroy" =>
"1"},{"name" => "T1"}}
Validation on task is done before destroying the old task. Hence validation fails.Any idea how to validate such that it doesn't consider task to be destroyed?
Andrew France created a patch in this thread, where the validation is done in memory.
class Author
has_many :books
# Could easily be made a validation-style class method of course
validate :validate_unique_books
def validate_unique_books
validate_uniqueness_of_in_memory(
books, [:title, :isbn], 'Duplicate book.')
end
end
module ActiveRecord
class Base
# Validate that the the objects in +collection+ are unique
# when compared against all their non-blank +attrs+. If not
# add +message+ to the base errors.
def validate_uniqueness_of_in_memory(collection, attrs, message)
hashes = collection.inject({}) do |hash, record|
key = attrs.map {|a| record.send(a).to_s }.join
if key.blank? || record.marked_for_destruction?
key = record.object_id
end
hash[key] = record unless hash[key]
hash
end
if collection.length > hashes.length
self.errors.add_to_base(message)
end
end
end
end
As I understand it, Reiner's approach about validating in memory would not be practical in my case, as I have a lot of "books", 500K and growing. That would be a big hit if you want to bring all into memory.
The solution I came up with is to:
Place the uniqueness condition in the database (which I've found is always a good idea, as in my experience Rails does not always do a good job here) by adding the following to your migration file in db/migrate/:
add_index :tasks [ :project_id, :name ], :unique => true
In the controller, place the save or update_attributes inside a transaction, and rescue the Database exception. E.g.,
def update
#project = Project.find(params[:id])
begin
transaction do
if #project.update_attributes(params[:project])
redirect_to(project_path(#project))
else
render(:action => :edit)
end
end
rescue
... we have an exception; make sure is a DB uniqueness violation
... go down params[:project] to see which item is the problem
... and add error to base
render( :action => :edit )
end
end
end
For Rails 4.0.1, this issue is marked as being fixed by this pull request, https://github.com/rails/rails/pull/10417
If you have a table with a unique field index, and you mark a record
for destruction, and you build a new record with the same value as the
unique field, then when you call save, a database level unique index
error will be thrown.
Personally this still doesn't work for me, so I don't think it's completely fixed yet.
Rainer Blessing's answer is good.
But it's better when we can mark which tasks are duplicated.
class Project < ActiveRecord::Base
has_many :tasks, inverse_of: :project
accepts_nested_attributes_for :tasks, :allow_destroy => :true
end
class Task < ActiveRecord::Base
belongs_to :project
validates_each :name do |record, attr, value|
record.errors.add attr, :taken if record.project.tasks.map(&:name).count(value) > 1
end
end
Ref this
Why don't you use :scope
class Task < ActiveRecord::Base
validates_uniqueness_of :name, :scope=>'project_id'
end
this will create unique Task for each project.
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.