Destroy deep associated object on parent save, using assign_attributes - ruby-on-rails

I want to delete an deep associated record inside assign_attributes.
Screen is the only object I need to save, but the deep associated NoteMember object should get deleted on save of Screen object, if params[:delete] is true for that particular NoteMember object.
Following is the table structure:
MODELS
class Screen < ActiveRecord::Base
has_many :alerts
accepts_nested_attributes_for :alerts
end
class Alert < ActiveRecord::Base
has_many :notes
accepts_nested_attributes_for :notes
end
class Note < ActiveRecord::Base
has_many :note_members
accepts_nested_attributes_for :note_members
end
class NoteMember < ActiveRecord::Base
end
CONTROLLER
s = Screen.where(id: <some_id>).first
alert_attrs = []
params[:alerts].each do |a|
notes_attrs = []
params[:notes].each do |n|
note_member_attrs = []
params[:note_members].each do |nm|
# if nm[:delete] = true, I need to delete the note member on saving Screen object
note_member_attrs.push({
id: nm[:id],
visibility: nm[:visibility]
})
end
notes_attrs.push({
id: n[:id],
description: n[:description],
note_members_attributes: note_member_attrs
})
end
alert_attrs.push({
id: a[:id],
name: a[:name]
notes_attributes: notes_attrs
})
end
s.assign_attributes(
alerts_attributes: alert_attrs
)
s.save!
How can this be achieved?

You can use rails built-in destroy functionality:
accepts_nested_attributes_for :note_members, allow_destroy: true
and pass
note_members_attributes: [ { _destroy: true, id: 123 }]
for note_members that need to be deleted.

Related

how to use an instance of a model in another serializer

I'm stuck here and couldn't find solution to proceed my work,
I have 3 models: plans, days, and meals.
This is my Plan Controller I've managed to get the correct answer in the controller, I want it nested and inside the serializer because I'm using URL helper to retrieve my images URLs, is there a possible way to use the #plan.id inside the DaySerializer?
def meals
#plan = Plan.find(params[:id])
#days = #plan.days
#meals = Meal.where("plan_id = ? ", #plan.id)
render :json => { :plan => #plan, :days => #days,
:meals => #meals }
end
This is my Plan model
class Plan < ApplicationRecord
has_many :days
has_one_attached :image, dependent: :destroy
end
This is my Day model
class Day < ApplicationRecord
has_many :meals
has_many :plans
end
This is my Meal model
class Meal < ApplicationRecord
belongs_to :plan
belongs_to :day
has_one_attached :image, dependent: :destroy
end
I want to show all meals for a specific Plan, to do that I need to use a variable inside the daySerializer but I couldn't find how to do it.
This is my planSerializer
class PlanSerializer < ActiveModel::Serializer
attributes :id, :name, :monthly_price, :plan_days
def plan_days
object.days.map do |day|
DaySerializer.new(day, scope: scope, root: false, event: object)
end
end
end
and this is my DaySerializer which I need to use the instance of the plan inside
class DaySerializer < ActiveModel::Serializer
attributes :number, :plan_meals
def plan_meals
#how to be able to use this line in Serilizer? !important
#plan = Plan.find(params[:id])
object.meals.map do |meal|
if meal.plan_id == #plan.id
MealSerializer.new(meal, scope: scope, root: false, event: object)
end
end
end
end
target reason response :
{
id: 8,
name: "Plan1",
monthly_price: 88,
plan_days: [
{
number: 5,
plan_meals: [],
},
{
number: 4,
plan_meals: [],
},
{
number: 3,
plan_meals: [],
},
{
number: 2,
plan_meals: [],
},
{
number: 1,
plan_meals: [
{
id: 11,
name: "test meal",
calories: 32,
protein: 32,
fat: 32,
carbohydrates: 32,
plan_id: 8,
},
],
},
],
}
currently it's showing all meals that belongs to each day,
not only the meals with the plan_id = Plan.find(params[:id])
In general I think you could use something like this should work.
ActiveModel::Serializer::CollectionSerializer.new. It actually by itself allows you to pass additional information to your serializer. It does the same as your current code just you are able to explicitly pass new data.
Controller:
def meals
#plan = Plan.find(params[:id])
#days = #plan.days
#meals = Meal.where("plan_id = ? ", #plan.id)
render :json => {
:plan => #plan,
:days => ActiveModel::Serializer::CollectionSerializer.new(#days, serializer: DaySerializer, plan_id: #plan.id),
:meals => #meals
}
end
And then in DaySerializer:
class DaySerializer < ActiveModel::Serializer
attributes :number, :plan_meals
def plan_meals
object.meals.map do |meal|
if meal.plan_id == instance_options[:plan_id]
MealSerializer.new(meal, scope: scope, root: false, event: object)
end
end
end
end
So in short ActiveModel::Serializer::CollectionSerializer.new in controller and instance_options in serializer to access passed additional parameters.
UPDATED:
How about add meal serializer?
class MealSerializer < ActiveModel::Serializer
attributes :id, :name, :calories, :protein, :fat, # etc
end
class DaySerializer < ActiveModel::Serializer
attributes :number
has_many :meals, serializer: MealSerializer
end
ORIGINAL:
class PlanSerializer < ActiveModel::Serializer
attributes :id, :name, :monthly_price, :plan_days
has_many :plan_days, serializer: DaySerializer
end
something like this.

Get id, name from master table - Ruby

The association is has follows
Company has_many company_commodities
CompanyCommodity belongs to Company
CompanyCommodity belongs to Commodity
Consider that company1 has an entry in the company_commodities table.
Now in the decorator file, i need to get the commodity name and id of that record.
I have implemented as follows.
company1 = Company.find(1)
arr = co.company_commodities.map(&:commodity).pluck(:name, :id)
arr.map { |a| { name: a[0], id: a[1] } }
This produces the output as
[{:name=>"Pharmaceuticals", :id=>25},
{:name=>"Medical Devices", :id=>26}]
Is there a cleaner way to do this? 
class Company < ApplicationRecord
has_many :company_commodities
has_many :commodities, through: :company_commodities
end
class CompanyCommodity < ApplicationRecord
belongs_to :commodity
belongs_to :company
end
company = Company.find(1)
# this returns the object you can access by arr.first.id, arr.first.name
arr = company.commodities.select(:name, :id)
# if you want to access as hash, usage: arr.first['id'], arr.first['name']
arr = company.commodities.select(:name, :id).as_json
You can get the commodities association by using through, then you can use select to filter the attributes.
Change the associations as below
class Company
has_many :company_commodities
has_many :commodities, through: :company_commodities
end
class CompanyCommodity
belongs_to :company
belongs_to :commodity
end
And query the records as
company1 = Company.find(1)
arr = co.commodities.pluck(:name, :id)
arr.reduce([]) { |array, el| array << [[:name, :id], el].to_h}

How to unscope multiple models in rails?

I am trying to unscope multiple model as below
User Model which has acts_as_paranoid
class User
acts_as_paranoid
has_one :category
has_one :brand
has_one :item
INDEXED_FIELDS = {
only: [:name],
include: {
category: { only: [:name] },
item: { only:[:name] },
brand: { only: [:name]},
}
}
def custom_json
Category.unscoped do
Item.unscoped do
Brand.unscoped do
self.as_json(INDEXED_FIELDS)
end
end
end
end
end
User model has following association which also has acts_as_paranoid
Sample Category model, Brand and Item model have same code
class Category
acts_as_paranoid
belongs_to :user
end
Can I do this dynamically with 'N' number of models, like iterating over array as below
def custom_json
[Category, Item, Brand].each do
# do unscoping
end
end
Association looks like
I think the approach you may have is to unscope the class manually, by setting default_scopes to [], and then putting it back.
classes_to_unscope = [Category, Item, Brand]
# remove default_scopes, saving them in previous_scopes
previous_scopes = classes_to_unscope.map do |klazz|
scopes = klazz.default_scopes
klazz.default_scopes = []
scopes
end
self.as_json(INDEXED_FIELDS)
# put default_scopes back
classes_to_unscope.each_with_index do |klazz, i|
klazz.default_scopes = previous_scopes[i]
end
As extra method:
def unscope_all(*models, &block)
# the order does not matter, but preserve it
blocks = [block] + models.reverse.map do |model|
proc do |inner_block|
model.unscoped { inner_block.call }
end
end
blocks.inject do |inner, outer|
proc { outer.call(inner) }
end.call
end
Then you would use it:
unscope_all(Category, Item, Brand) do
# do unscoping
end
unscoped pitfall: when leaving the block you loose the "unscopability", so make sure you don't return a relation (it won't be unscoped). Instead you have to resolve it in the block (e.g. by returning an array where(...).to_a.

Rails - How to a validate model being altered in before filter from a different model?

Models - Purchaseorder, Purchaseorderadjustments, Productvariant, Location, Locationinventory
I'm storing inventory in Locationinventory which stores a location_id, productvariant_id, and quantity.
The situation arises when I want to create a purchaseorder. I'm using purchaseorderadjustments as a nested attribute to the purchaseorder. A purchaseorder has_many purchaseorderadjustments that store the productvariant_id and quantity.
I'm using before filters to create,update and destroy the related locationinventory records. Everything works well as it is now except that you can remove items from a location that doesn't have them available and the quantity just goes into the negative. I want to verify that the "From Location" has enough of the productvariant in stock to transfer to the "To Location".
Am I doing it wrong? thanks!
Rails 3.2.14
Purchaseorder.rb
class Purchaseorder < ActiveRecord::Base
attr_accessible :fromlocation_id, :status_id, :tolocation_id, :user_id, :purchaseorderadjustments_attributes
belongs_to :user
belongs_to :status
belongs_to :fromlocation, :class_name => "Location", :foreign_key => :fromlocation_id
belongs_to :tolocation, :class_name => "Location", :foreign_key => :tolocation_id
has_many :purchaseorderadjustments, :dependent => :destroy
accepts_nested_attributes_for :purchaseorderadjustments, allow_destroy: true
end
Purchaseorderadjustment.rb
class Purchaseorderadjustment < ActiveRecord::Base
attr_accessible :adjustmenttype_id, :productvariant_id, :purchaseorder_id, :quantity
belongs_to :purchaseorder
belongs_to :productvariant
belongs_to :adjustmenttype
validates_presence_of :quantity, :message => "You need a quantity for each product."
# On creation of a purchaseorderadjustment go ahead and create the record for locationinventory
before_create :create_locationinventory
def create_locationinventory
# Get some info before updating the locationinventory
if fromlocationinventory = Locationinventory.find(:first, conditions: { :location_id => purchaseorder.fromlocation_id, :productvariant_id => productvariant_id })
fromlocation_current_quantity = fromlocationinventory.quantity
end
if tolocationinventory = Locationinventory.find(:first, conditions: { :location_id => purchaseorder.tolocation_id, :productvariant_id => productvariant_id })
tolocation_current_quantity = tolocationinventory.quantity
end
# Create or update the from locationinventory
unless fromlocationinventory.nil?
fromlocationinventory.quantity = fromlocation_current_quantity - quantity
fromlocationinventory.save
else
new_fromlocationinventory = Locationinventory.new({ location_id: purchaseorder.fromlocation_id, productvariant_id: productvariant_id, quantity: 0 - quantity })
new_fromlocationinventory.save
end
# Create or update the to locationinventory
unless tolocationinventory.nil?
tolocationinventory.quantity = tolocation_current_quantity + quantity
tolocationinventory.save
else
new_tolocationinventory = Locationinventory.new({ location_id: purchaseorder.tolocation_id, productvariant_id: productvariant_id, quantity: quantity })
new_tolocationinventory.save
end
end
#On update of purchaseorderadjustment
before_update :update_locationinventory
def update_locationinventory
# Get some info before updating the locationinventory
fromlocationinventory = Locationinventory.find(:first, conditions: { :location_id => purchaseorder.fromlocation_id, :productvariant_id => productvariant_id })
tolocationinventory = Locationinventory.find(:first, conditions: { :location_id => purchaseorder.tolocation_id, :productvariant_id => productvariant_id })
fromlocation_current_quantity = fromlocationinventory.quantity
tolocation_current_quantity = tolocationinventory.quantity
fromlocationinventory.quantity = fromlocation_current_quantity - quantity + self.quantity_was
fromlocationinventory.save
tolocationinventory.quantity = tolocation_current_quantity + quantity - self.quantity_was
tolocationinventory.save
end
#On destroy of purchaseorderadjustment
before_destroy :destroy_locationinventory
def destroy_locationinventory
# Get some info before updating the locationinventory
fromlocationinventory = Locationinventory.find(:first, conditions: { :location_id => purchaseorder.fromlocation_id, :productvariant_id => productvariant_id })
tolocationinventory = Locationinventory.find(:first, conditions: { :location_id => purchaseorder.tolocation_id, :productvariant_id => productvariant_id })
fromlocation_current_quantity = fromlocationinventory.quantity
tolocation_current_quantity = tolocationinventory.quantity
fromlocationinventory.quantity = fromlocation_current_quantity + quantity
fromlocationinventory.save
tolocationinventory.quantity = tolocation_current_quantity - quantity
tolocationinventory.save
end
end
productvariant.rb
class Productvariant < ActiveRecord::Base
attr_accessible :barcode, :compare_at_price, :fulfillment_service, :grams,
:inventory_management, :inventory_policy, :inventory_quantity,
:option1, :option2, :option3, :position, :price, :product_id,
:requires_shipping, :shopify_id, :sku, :taxable, :title, :shopify_product_id, :product_title
belongs_to :product, primary_key: "shopify_id", foreign_key: "shopify_product_id"
has_many :purchaseorderadjustments
has_many :locationinventories
def product_plus_variant
"#{self.product.title} - #{self.title}"
end
end
locationinventory.rb
class Locationinventory < ActiveRecord::Base
attr_accessible :location_id, :productvariant_id, :quantity
belongs_to :productvariant
belongs_to :location
end
I'll write this answer because I feel you've provided so much code, you might have scared some answerers away!
Our experience is as follows:
Nested
You can validate nested models in several different ways
Your question is related to passing data in a accepts_nested_attributes_for - you can validate this directly:
#app/models/purchase.rb
Class Purchase < ActiveRecord::Base
has_many :purchase_items
accepts_nested_attributes_for :purchase_items
end
#app/models/purchase_item.rb
Class PurchaseItem < ActiveRecord::Base
belongs_to :purchase
validates :name,
presence: { message: "Your Purchase Needs Items!" } #Returns to initial form with this error
end
Standard
If you want to conditionally validate based on another model, you'll have to use inverse_of: to keep the object available throughout the data transaction:
#app/models/purchase.rb
Class Purchase < ActiveRecord::Base
has_many :purchase_items, inverse_of: :purchase
accepts_nested_attributes_for :purchase_items
end
#app/models/purchase_item.rb
Class PurchaseItem < ActiveRecord::Base
belongs_to :purchase, inverse_of: :purchase_items
validates :name,
presence: { message: "Your Purchase Needs Items!" },
if: :paid_with_card?
private
def paid_with_card?
self.purchase.payment_method == "card"
end
end

rspec association creation error

I have a model item has_many ratings and a ratings belongs_to item ratings belongs_to user I want to force a user who is creating an item to rate it too. Other users can then rate it later on. item and user have no association in my model.
I am doing the following in my item_spec which is giving me an error no implicit conversion of Symbol into Integer on line #item = Item.new(name: "Item1", below.
class Item < ActiveRecord::Base
has_many :ratings, dependent: :destroy, inverse_of: :item
accepts_nested_attributes_for :ratings, :allow_destroy => true
validates :name , :length => { minimum: 3 }
validates :category , :length => { minimum: 3 }
validates_presence_of :ratings
end
require 'spec_helper'
describe Item do
before do
#item = Item.new(name: "Item1",
url: "www.item1.com",
full_address: "Item1Address",
city: "Item1City",
country: "Item1Country",
category: "Item1Type",
ratings_attributes: {"rating" => "3", "comment" => "Ahh Good"} )
end
Also using FactoryGirl I am doing something like this
factory :item do
before_create do |r|
r.ratings<< FactoryGirl.build(:ratings, item: r )
end
name "Item1"
url "www.Item1.com"
full_address "Item1Address"
city "Item1City"
country "Item1Country"
category "Item1Category"
end
factory :ratings do
rating 3
comment "Its not that bad"
user
end
end
which again is not yeilding the desired result.
can anyone help me solve this problem please.Thanks!
Working Code, now having problem testing some association order, but at least the desired functionality working.
factory :item do
name "Item1"
url "www.Item1.com"
full_address "Item1Address"
city "Item1City"
country "Item1Country"
category "Item1Category"
end
factory :ratings, :class => 'Ratings' do
association :item, factory: :item, strategy: :build
user
rating 3
comment "Its not that bad"
end
factory :item_with_rating, parent: :item do
ratings {[FactoryGirl.create(:ratings)]}
end
Here is the spec file
require 'spec_helper'
describe Item do
before do
#item = FactoryGirl.create(:item_with_rating)
end
subject { #item }
it { should respond_to(:name) }
it { should respond_to(:url) }
it { should respond_to(:full_address)}
it { should respond_to(:city) }
it { should respond_to(:country) }
it { should respond_to(:category) }
it { should respond_to(:ratings) }
it { should_not respond_to(:type) }
it { should_not respond_to(:user_id) }
it { should be_valid }
There is no change in the Model file for item

Resources