Rails accept_nested_attributes still throws "Can't mass-assign protected attributes" - ruby-on-rails

I think I'm either missing something really simple or something really obscure. Hoping someone can spot it for me or explain my muppetry.
Ok, So there are two models, Basket and BasketItem.
I've set Basket to accept_nested_attributes :basket_items with the intention of using fields_for in an edit view of Basket.
However when run up it still screams that
Error: Can't mass-assign protected attributes: basket_items_attributes
For the sake of this question I've boiled down to the same issue if I do a manual basket.update_attributes in the console with just one or two basket_item attributes. So I know it's a model issue, not a view or controller issue.
e.g.:
basket.update_attributes("basket_items_attributes"=>[{"qty"=>"1", "id"=>"29"}, {"qty"=>"7", "id"=>"30"}])
or similarly with a hash more like fields_for makes
basket.update_attributes( "basket_items_attributes"=>{
"0"=>{"qty"=>"1", "id"=>"29"},
"1"=>{"qty"=>"7", "id"=>"30"}
})
I've ensured that the associates in defined before the accepts_nested_attibutes_for, that the child model has the appropriate attributes accesable too, tried removing additional attributes for the nested data, lots of fiddling to no avail.
basket.rb
class Basket < ActiveRecord::Base
has_many :basket_items
attr_accessible :user_id
accepts_nested_attributes_for :basket_items
belongs_to :user
def total
total = 0
basket_items.each do |line_item|
total += line_item.total
end
return total
end
# Add new Variant or increment existing Item with new Quantity
def add_variant(variant_id = nil, qty = 0)
variant = Variant.find(variant_id)
# Find if already listed
basket_item = basket_items.find(:first, :conditions => {:variant_id => variant.id})
if (basket_item.nil?) then
basket_item = basket_items.new(:variant => variant, :qty => qty)
else
basket_item.qty += qty
end
basket_item.save
end
end
basket_item.rb
class BasketItem < ActiveRecord::Base
belongs_to :basket
belongs_to :variant
attr_accessible :id, :qty, :variant, :basket_id
def price
variant.price
end
def sku
return variant.sku
end
def description
variant.short_description
end
def total
price * qty
end
end

As the error says, you just need to add basket_items_attributes to your list of accepted attributes.
So you'd have
attr_accessible :user_id, :basket_items_attributes
at the top of your basket.rb file

Related

Basic rule to store User ID into another table as foreign key in ruby on rails

As a beginner I thought that it would be easy to store current user session ID into another table as I managed to get the user ID into Line_Items table however I believe that I have not understood the basic principle of storing one primary key into another table as a foreign key which in my case is trying to store session User_ID into Carts table as a cart belongs to a user and user has many cart. Any code or any helpful link regarding this question will be greatly appreciated. Following are my models. Please let me know if any other code is needed for assistance. Thanks:
class User < ActiveRecord::Base
attr_accessible :email, :first_name, :last_name, :password, :role, :subscriber, :name
has_many :line_item
has_many :cart
class NotAuthorized < StandardError; end
end
Cart.rb:
class Cart < ActiveRecord::Base
attr_accessible :user_id
has_many :line_items, dependent: :destroy
belongs_to :user
def add_phone(phone, current_user = nil)
current_item = line_items.find_or_initialize_by_phone_id(phone.id)
current_item.increment!(:quantity)
current_item.phone.decrement!(:stock)
current_item.user = current_user
current_item.phone.save
current_item
end
# returns true if stock level is greater than zero
def can_add(phone)
phone.stock > 0
end
# returns the total number of items in a cart
def number_of_items
total = 0
line_items.each do |item|
total += item.quantity
end
total
end
def empty?
number_of_items == 0
end
def grand_total
grand_total = 0
line_items.each do |item|
grand_total += item.quantity * item.phone.price
end
grand_total
end
end
You have an error in your relationship declaration.. they should be plural, try that and see if it helps, also you should have an user_id on your carts & line items models.
has_many :line_items
has_many :carts
In User model
has_many :line_items
has_many :carts
Also in line Item and Cart model, you can mention:
belongs_to :user
We can mentioned it using :foreign_key as well given below:
belongs_to :user, foreign_key: "user_id"
Hope it will be helpful for you..
For getting session ID or for saving the records in your cart controller or line items controller
For LineItem
#In create action
#line_item = current_user.line_items.build(params[:user])
# or
#line_item.user = current_user
For Cart
#In create action
#cart = current_user.carts.build(params[:user])
# or
#cart.user = current_user
For getting user id in your controller for finding related line_items and cart you can simply get it by current_user.id
In controllers or views for session id you can use as below
session['session_id']
Thanks.

Order by field in related model with ActiveRecord condition

I am trying to order by a field in a related model in Rails. All of the solutions I have researched have not addressed if the related model is filtered by another parameter?
Item model
class Item < ActiveRecord::Base
has_many :priorities
Related Model:
class Priority < ActiveRecord::Base
belongs_to :item
validates :item_id, presence: true
validates :company_id, presence: true
validates :position, presence: true
end
I am retrieving Items using a where clause:
#items = Item.where('company_id = ? and approved = ?', #company.id, true).all
I need to order by the 'Position' column in the related table. The trouble has been that in the Priority model, an item could be listed for multiple companies. So the positions are dependent on which company_id they have. When I display the items, it is for one company, ordered by position within the company. What is the proper way to accomplish this? Any help is appreciated.
PS - I am aware of acts_as_list however found it did not quite suit my setup here, so I am manually handling saving the sorting while still using jquery ui sortable.
You could use the includes method to include the build association then order by it. You just make sure you disambiguate the field you are ordering on and there are some things you should read up on here on eager loading. So it could be something like:
#items = Item.includes(:priorities).where('company_id = ? and approved = ?', #company.id, true).order("priorities.position ASC")
class Item < ActiveRecord::Base
has_many :priorities
belongs_to :company
def self.approved
where(approved: true)
end
end
class Priority < ActiveRecord::Base
belongs_to :item
end
class Company < ActiveRecord::Base
has_many :items
end
#company = Company.find(params[:company_id])
#items = #company.items.joins(:priorities).approved.order(priorities: :position)
If I've understood your question, that's how I'd do it. It doesn't really need much explanation but lemme know if you're not sure.
If you wanted to push more of it into the model, if it's a common requirement, you could scope the order:
class Item < ActiveRecord::Base
has_many :priorities
belongs_to :company
def self.approved
where(approved: true)
end
def self.order_by_priority_position
joins(:priorities).order(priorities: :position)
end
end
and just use: #company.items.approved.order_by_priority_position

rails - validate product ordered has the available quantity

I have two related validations on separate models. I can validate one way but not another. The post validation works: if I try to change the quantity available as the seller, and people have already ordered some of it, it (correctly) won't let me change the quantity below the amount ordered already.
The order validation does not work: if I try changing the order amount to more than is available, it lets me. It checks only the current sum (I guess before the save), and does not notice the buyer trying to sneak out more than what's available. When I try and change the order back to less than what is available, it won't let me, checking the current sum ordered (before save).
I also tried validating the quantity of the order + the remaining available stock (available -sum ordered), but have the same problem.
How would I get the validation to check what the quantity would be after the save, and then not save it should the value be invalid? It would have to work also for editing an order
class Post < ActiveRecord::Base
belongs_to :product
belongs_to :event
has_many :orders, :dependent => :destroy
attr_accessible :quantity, :deadline, :product_id, :event_id
validate :quantity_gt_ordered
def quantity_gt_ordered
self.errors.add(:quantity, " - People have already ordered more than this") unless self.quantity >= self.sum_orders
end
def sum_orders
self.orders.sum(:quantity)
end
class Order < ActiveRecord::Base
belongs_to :user
belongs_to :post
attr_accessible :order_price, :post_id, :user_id, :quantity
validate :quantity_is_available
def quantity_is_available
self.errors.add(:quantity, " - Please order only what's available") unless self.sum_post_orders <= self.post.quantity
end
def sum_post_orders
Order.where(:post => self.post).sum(:quantity)
end
You should just be able to compare to the available quantity minus the ordered quantity:
available_quantity = self.post.quantity - self.sum_post_order
if quantity > available_quantity
self.errors.add(:quantity, " - Please order only what's available")
end
And also make sure your sum_post_orders doesn't include the current order:
Order.where("id != ?", self.id).where(:post => self.post).sum(:quantity)

validates_uniqueness_of in destroyed nested model rails

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.

Model Relationship Problem

I am trying to calculate the average (mean) rating for all entries within a category based on the following model associations ...
class Entry < ActiveRecord::Base
acts_as_rateable
belongs_to :category
...
end
class Category < ActiveRecord::Base
has_many :entry
...
end
class Rating < ActiveRecord::Base
belongs_to :rateable, :polymorphic => true
...
end
The rating model is handled by the acts as rateable plugin, so the rateable model looks like this ...
module Rateable #:nodoc:
...
module ClassMethods
def acts_as_rateable
has_many :ratings, :as => :rateable, :dependent => :destroy
...
end
end
...
end
How can I perform the average calculation? Can this be accomplished through the rails model associations or do I have to resort to a SQL query?
The average method is probably what you're looking for. Here's how to use it in your situation:
#category.entries.average('ratings.rating', :joins => :ratings)
Could you use a named_scope or custom method on the model. Either way it would still require some SQL since, if I understand the question, your are calculating a value.
In a traditional database application this would be a view on the data tables.
So in this context you might do something like... (note not tested or sure it is 100% complete)
class Category
has_many :entry do
def avg_rating()
#entries = find :all
#entres.each do |en|
#value += en.rating
end
return #value / entries.count
end
end
Edit - Check out EmFi's revised answer.
I make no promises but try this
class Category
def average_rating
Rating.average :rating,
:conditions => [ "type = ? AND entries.category_id = ?", "Entry", id ],
:join => "JOIN entries ON rateable_id = entries.id"
end
end

Resources