accepts_nested_attributes_for: destroy all not included in array - ruby-on-rails

Is there a clean way to destroy all children NOT included in array of passed nested attributes?
Now I have to find difference between actual children and nested attributes array, and then set _destroy: true for each, but it looks ugly.
class Report < ActiveRecord::Base
has_many :consumed_products
accepts_nested_attributes_for :consumed_products, allow_destroy: true
def nested_attributes_destroy_difference(attrs)
combined = attrs.reduce({}) {|h,pairs| pairs.each {|k,v| (h[k] ||= []) << v}; h}
diff = consumed_products - consumed_products.where(combined)
attrs + diff.map{|i| {id: i.id, _destroy: true} }
end
end
class Api::V2::ReportsController < Api::V2::BaseController
def update
report = Report.find(params[:id])
report_attributes = report_params
if params[:consumed_products]
report_attributes.merge!(consumed_products_attributes: report.nested_attributes_destroy_difference(consumed_products_attributes))
end
report.assign_attributes report_attributes
end
private
def consumed_products_attributes
params[:consumed_products].map do |p|
{product_id: p[:id], product_measure_id: p[:measure_id], quantity: p[:quantity]}
end
end
def report_params
#...
end
end

Related

change the name of nested keys received as parameter

Hi in rails the nested parameters are passed with attributes appended to the key's and then its is passed to permit
If I receive normal hash and want to append attribute to each nested key before permit is called
How to do that ?
"project":{
"project_name":"test",
"tentative_start_date":"2018-12-12",
"tentative_end_date":"2019-12-12",
"project_roles":[
{
"role_id":1,
"project_role_skills":[
{
"skill":{
"skill_type":"C++",
"id":2
}
}
],
"project_role_users":[
],
"role_end_date":"2018-12-12",
"role_start_date":"2018-12-12"
}
]
}
}
This is the request I received and I want to append attribute to project_roles,project_role_skill and so on so that rails accept that
Please anyone help
Have you tried this? Rails 4 - Strong Parameters - Nested Objects
If it doesn't work for you, can you please specify what is your form currently and what is your current method using permit?
TL;DR:
def params_with_deep_appended_nested_attributes(_params)
modified_params = _params.clone
_params.each do |k, v|
if v.is_a?(Array)
modified_params["#{k}_attributes"] = []
v.each_with_index do |vv, index|
if vv.is_a?(ActionController::Parameters)
modified_params["#{k}_attributes"][index] = params_with_deep_appended_nested_attributes(vv)
end
end
modified_params.delete(k)
elsif v.is_a?(ActionController::Parameters)
modified_params["#{k}_attributes"] = params_with_deep_appended_nested_attributes(v)
modified_params.delete(k)
end
end
modified_params
end
Usage Example:
# rails console
example_json_hash_request = {
"project": {
"project_name":"test",
"tentative_start_date":"2018-12-12",
"tentative_end_date":"2019-12-12",
"project_roles": [
{
"role_id":1,
"project_role_skills":[
{
"skill":{
"skill_type":"C++",
"id":2
}
}
],
"project_role_users":[
],
"role_end_date":"2018-12-12",
"role_start_date":"2018-12-12"
}
]
}
}
# simulate `params` value in the controller
params = ActionController::Parameters.new(example_json_hash_request)
modified_params = params_with_deep_appended_nested_attributes(params)
pp modified_params.permit!.to_hash
# {"project_attributes"=>
# {"project_name"=>"test",
# "tentative_start_date"=>"2018-12-12",
# "tentative_end_date"=>"2019-12-12",
# "project_roles_attributes"=>
# [{"role_id"=>1,
# "role_end_date"=>"2018-12-12",
# "role_start_date"=>"2018-12-12",
# "project_role_skills_attributes"=>
# [{"skill_attributes"=>{"skill_type"=>"C++", "id"=>2}}],
# "project_role_users_attributes"=>[]}]}}
Solution:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# ...
# I added `= params` to default to the `params` value here in the controller
def params_with_deep_appended_nested_attributes(_params = params)
modified_params = _params.clone
_params.each do |k, v|
if v.is_a?(Array)
modified_params["#{k}_attributes"] = []
v.each_with_index do |vv, index|
if vv.is_a?(ActionController::Parameters)
modified_params["#{k}_attributes"][index] = params_with_deep_appended_nested_attributes(vv)
end
end
modified_params.delete(k)
elsif v.is_a?(ActionController::Parameters)
modified_params["#{k}_attributes"] = params_with_deep_appended_nested_attributes(v)
modified_params.delete(k)
end
end
modified_params
end
# ...
end
# app/controllers/projects_controller.rb
class ProjectsController < ApplicationController
def create
#project = Project.new(project_params)
if #project.save
# ...
else
# ...
end
end
def project_params
params_with_deep_appended_nested_attributes.require(:project_attributes).permit(
:project_name, :tentative_start_date, :tentative_end_date,
project_roles_attributes: [
:role_id, :role_end_date, :role_start_date,
project_role_skills_attributes: [
skill_attributes: [
:skill_type, :id
]
],
project_role_users_attributes: []
]
)
end
end
Don't forget to define the "nested attributes" in the models:
# app/models/project.rb
class Project < ApplicationRecord
has_many :project_roles
accepts_nested_attributes_for :project_roles
end
# app/models/project_role.rb
class ProjectRole < ApplicationRecord
belongs_to :project
has_many :project_role_skills
has_many :project_role_users
accepts_nested_attributes_for :project_role_skills
accepts_nested_attributes_for :project_role_users
end
# app/models/project_role_skill.rb
class ProjectRoleSkill < ApplicationRecord
belongs_to :project_role
belongs_to :skill
accepts_nested_attributes_for :skill
end
TODOs:
add cahing of the params_with_deep_appended_nested_attributes return value, to avoid running the code each time params_with_deep_appended_nested_attributes is going to be called.
This question interests me. I might find use to this code of mine in the future.

How can I test these RSS parsing service objects?

I have some service objects that use Nokogiri to make AR instances. I created a rake task so that I can update the instances with a cron job. What I want to test is if it's adding items that weren't there before, ie:
Create an Importer with a url of spec/fixtures/feed.xml, feed.xml having 10 items.
Expect Show.count == 1 and Episode.count == 10
Edit spec/fixtures/feed.xml to have 11 items
Invoke rake task
Expect Show.count == 1 and Episode.count == 11
How could I test this in RSpec, or modify my code to be more testable?
# models/importer.rb
class Importer < ActiveRecord::Base
after_create :parse_importer
validates :title, presence: true
validates :url, presence: true
validates :feed_format, presence: true
private
def parse_importer
Parser.new(self)
end
end
# models/show.rb
class Show < ActiveRecord::Base
validates :title, presence: true
validates :title, uniqueness: true
has_many :episodes
attr_accessor :entries
end
# models/episode.rb
class Episode < ActiveRecord::Base
validates :title, presence: true
validates :title, uniqueness: true
belongs_to :show
end
#lib/tasks/admin.rake
namespace :admin do
desc "Checks all Importer URLs for new items."
task refresh: :environment do
#importers = Importer.all
#importers.each do |importer|
Parser.new(importer)
end
end
end
# services/parser.rb
class Parser
def initialize(importer)
feed = Feed.new(importer)
show = Show.where(rss_link: importer.url).first
if show # add new episodes
new_episodes = Itunes::Channel.refresh(feed.origin)
new_episodes.each do |new_episode|
show.episodes.create feed.episode(new_episode)
end
else # create a show and its episodes
new_show = Show.new(feed.show) if (feed && feed.show)
if (new_show.save && new_show.entries.any?)
new_show.entries.each do |entry|
new_show.episodes.create feed.episode(entry)
end
end
end
end
end
# services/feed.rb
class Feed
require "nokogiri"
require "open-uri"
require "formats/itunes"
attr_reader :params, :origin, :show, :episode
def initialize(params)
#params = params
end
def origin
#origin = Nokogiri::XML(open(params[:url]))
end
def format
#format = params[:feed_format]
end
def show
case format
when "iTunes"
Itunes::Channel.fresh(origin)
end
end
def episode(entry)
#entry = entry
case format
when "iTunes"
Itunes::Item.fresh(#entry)
end
end
end
# services/formats/itunes.rb
class Itunes
class Channel
def initialize(origin)
#origin = origin
end
def title
#origin.xpath("//channel/title").text
end
def description
#origin.xpath("//channel/description").text
end
def summary
#origin.xpath("//channel/*[name()='itunes:summary']").text
end
def subtitle
#origin.xpath("//channel/*[name()='itunes:subtitle']/text()").text
end
def rss_link
#origin.xpath("//channel/*[name()='atom:link']/#href").text
end
def main_link
#origin.xpath("//channel/link/text()").text
end
def docs_link
#origin.xpath("//channel/docs/text()").text
end
def release
#origin.xpath("//channel/pubDate/text()").text
end
def image
#origin.xpath("//channel/image/url/text()").text
end
def language
#origin.xpath("//channel/language/text()").text
end
def keywords
keywords_array(#origin)
end
def categories
category_array(#origin)
end
def explicit
explicit_check(#origin)
end
def entries
entry_array(#origin)
end
def self.fresh(origin)
#show = Itunes::Channel.new origin
return {
description: #show.description,
release: #show.release,
explicit: #show.explicit,
language: #show.language,
title: #show.title,
summary: #show.summary,
subtitle: #show.subtitle,
image: #show.image,
rss_link: #show.rss_link,
main_link: #show.main_link,
docs_link: #show.docs_link,
categories: #show.categories,
keywords: #show.keywords,
entries: #show.entries
}
end
def self.refresh(origin)
#show = Itunes::Channel.new origin
return #show.entries
end
private
def category_array(channel)
arr = []
channel.xpath("//channel/*[name()='itunes:category']/#text").each do |category|
arr.push(category.to_s)
end
return arr
end
def explicit_check(channel)
string = channel.xpath("//channel/*[name()='itunes:explicit']").text
if string === "yes" || string === "Yes"
true
else
false
end
end
def keywords_array(channel)
keywords = channel.xpath("//channel/*[name()='itunes:keywords']/text()").text
arr = keywords.split(",")
return arr
end
def entry_array(channel)
arr = []
channel.xpath("//item").each do |item|
arr.push(item)
end
return arr
end
end
class Item
def initialize(origin)
#origin = origin
end
def description
#origin.xpath("*[name()='itunes:subtitle']").text
end
def release
#origin.xpath("pubDate").text
end
def image
#origin.xpath("*[name()='itunes:image']/#href").text
end
def explicit
explicit_check(#origin)
end
def duration
#origin.xpath("*[name()='itunes:duration']").text
end
def title
#origin.xpath("title").text
end
def enclosure_url
#origin.xpath("enclosure/#url").text
end
def enclosure_length
#origin.xpath("enclosure/#length").text
end
def enclosure_type
#origin.xpath("enclosure/#type").text
end
def keywords
keywords_array(#origin.xpath("*[name()='itunes:keywords']").text)
end
def self.fresh(entry)
#episode = Itunes::Item.new entry
return {
description: #episode.description,
release: #episode.release,
image: #episode.image,
explicit: #episode.explicit,
duration: #episode.duration,
title: #episode.title,
enclosure_url: #episode.enclosure_url,
enclosure_length: #episode.enclosure_length,
enclosure_type: #episode.enclosure_type,
keywords: #episode.keywords
}
end
private
def explicit_check(item)
string = item.xpath("*[name()='itunes:explicit']").text
if string === "yes" || string === "Yes"
true
else
false
end
end
def keywords_array(item)
keywords = item.split(",")
return keywords
end
end
end
Before anything else, good for you for using service objects! I've been using this approach a great deal and find POROs preferable to fat models in many situations.
It appears the behavior you're interested in testing is contained in Parser.initialize.
First, I'd create a class method for Parser called parse. IMO, Parser.parse(importer) is clearer about what Parser is doing than is Parser.new(importer). So, it might look like:
#services/parser.rb
class Parser
class << self
def parse(importer)
#importer = importer
#feed = Feed.new(importer)
if #show = Show.where(rss_link: importer.url).first
create_new_episodes Itunes::Channel.refresh(#feed.origin)
else
create_show_and_episodes
end
end # parse
end
end
Then add the create_new_episodes and create_show_and_episodes class methods.
#services/parser.rb
class Parser
class << self
def parse(importer)
#importer = importer
#feed = Feed.new(importer)
if #show = Show.where(rss_link: #importer.url).first
create_new_episodes Itunes::Channel.refresh(#feed.origin)
else
create_show_and_episodes
end
end # parse
def create_new_episodes(new_episodes)
new_episodes.each do |new_episode|
#show.episodes.create #feed.episode(new_episode)
end
end # create_new_episodes
def create_show_and_episodes
new_show = Show.new(#feed.show) if (#feed && #feed.show)
if (new_show.save && new_show.entries.any?)
new_show.entries.each do |entry|
new_show.episodes.create #feed.episode(entry)
end
end
end # create_show_and_episodes
end
end
Now you have a Parser.create_new_episodes method that you can test independently. So, your test might look something like:
require 'rspec_helper'
describe Parser do
describe '.create_new_episodes' do
context 'when an initial parse has been completed' do
before(:each) do
first_file = Nokogiri::XML(open('spec/fixtures/feed_1.xml'))
#second_file = Nokogiri::XML(open('spec/fixtures/feed_2.xml'))
Parser.create_show_and_episodes first_file
end
it 'changes Episodes.count by 1' do
expect{Parser.create_new_episodes(#second_file)}.to change{Episodes.count}.by(1)
end
it 'changes Show.count by 0' do
expect{Parser.create_new_episodes(#second_file)}.to change{Show.count}.by(0)
end
end
end
end
Naturally, you'll need feed_1.xml and feed_2.xml in the spec\fixtures directory.
Apologies for any typos. And, I didn't run the code. So, might be buggy. Hope it helps.

NameError: uninitialized constant Shipment with Rspec

I'm trying to write rspec test cases with rspec-mock. But, I got an error as follow. order_decorator_spec.rb:149 is let!(:order) { create(:order, shipments: [shipment]) }. It doesn't show any stacktrace perhaps bacause it is executed on RubyMine console. How can I fix the problem??
console
NameError: uninitialized constant Shipment
./spec/models/spree/order_decorator_spec.rb:149:in `block (3 levels) in <top (required)>'
order_decorator_spec.rb
require 'spec_helper'
describe Spree::Order do
context '#after_cancel' do
let!(:shipment) { double(:shipment, :cancel! => '')}
before {shipment}
let!(:order) { create(:order, shipments: [shipment]) }
before do
order.stub(:after_cancel)
end
it "should adsfasdfasdfa" do
shipment.should_receive(:cancel!).once
order.after_cancel
end
end
end
order.rb
require 'spree/core/validators/email'
require 'spree/order/checkout'
module Spree
class Order < ActiveRecord::Base
include Checkout
checkout_flow do
go_to_state :address
go_to_state :delivery
go_to_state :payment, if: ->(order) {
order.update_totals
order.payment_required?
}
go_to_state :confirm, if: ->(order) { order.confirmation_required? }
go_to_state :complete
remove_transition from: :delivery, to: :confirm
end
token_resource
attr_reader :coupon_code
if Spree.user_class
belongs_to :user, class_name: Spree.user_class.to_s
belongs_to :created_by, class_name: Spree.user_class.to_s
else
belongs_to :user
belongs_to :created_by
end
belongs_to :bill_address, foreign_key: :bill_address_id, class_name: 'Spree::Address'
alias_attribute :billing_address, :bill_address
belongs_to :ship_address, foreign_key: :ship_address_id, class_name: 'Spree::Address'
alias_attribute :shipping_address, :ship_address
has_many :state_changes, as: :stateful
has_many :line_items, -> { order('created_at ASC') }, dependent: :destroy
has_many :payments, dependent: :destroy
has_many :return_authorizations, dependent: :destroy
has_many :adjustments, -> { order("#{Adjustment.table_name}.created_at ASC") }, as: :adjustable, dependent: :destroy
has_many :line_item_adjustments, through: :line_items, source: :adjustments
has_many :all_adjustments, class_name: 'Spree::Adjustment'
has_many :inventory_units
has_many :shipments, dependent: :destroy do
def states
pluck(:state).uniq
end
end
accepts_nested_attributes_for :line_items
accepts_nested_attributes_for :bill_address
accepts_nested_attributes_for :ship_address
accepts_nested_attributes_for :payments
accepts_nested_attributes_for :shipments
# Needs to happen before save_permalink is called
before_validation :set_currency
before_validation :generate_order_number, on: :create
before_validation :clone_billing_address, if: :use_billing?
attr_accessor :use_billing
before_create :link_by_email
after_create :create_tax_charge!
validates :email, presence: true, if: :require_email
validates :email, email: true, if: :require_email, allow_blank: true
validate :has_available_shipment
validate :has_available_payment
make_permalink field: :number
class_attribute :update_hooks
self.update_hooks = Set.new
def self.by_number(number)
where(number: number)
end
def self.between(start_date, end_date)
where(created_at: start_date..end_date)
end
def self.by_customer(customer)
joins(:user).where("#{Spree.user_class.table_name}.email" => customer)
end
def self.by_state(state)
where(state: state)
end
def self.complete
where.not(completed_at: nil)
end
def self.incomplete
where(completed_at: nil)
end
# Use this method in other gems that wish to register their own custom logic
# that should be called after Order#update
def self.register_update_hook(hook)
self.update_hooks.add(hook)
end
# For compatiblity with Calculator::PriceSack
def amount
line_items.inject(0.0) { |sum, li| sum + li.amount }
end
def currency
self[:currency] || Spree::Config[:currency]
end
def display_outstanding_balance
Spree::Money.new(outstanding_balance, { currency: currency })
end
def display_item_total
Spree::Money.new(item_total, { currency: currency })
end
def display_adjustment_total
Spree::Money.new(adjustment_total, { currency: currency })
end
def display_tax_total
Spree::Money.new(tax_total, { currency: currency })
end
def display_ship_total
Spree::Money.new(ship_total, { currency: currency })
end
def display_total
Spree::Money.new(total, { currency: currency })
end
def to_param
number.to_s.to_url.upcase
end
def completed?
completed_at.present? || complete?
end
# Indicates whether or not the user is allowed to proceed to checkout.
# Currently this is implemented as a check for whether or not there is at
# least one LineItem in the Order. Feel free to override this logic in your
# own application if you require additional steps before allowing a checkout.
def checkout_allowed?
line_items.count > 0
end
# Is this a free order in which case the payment step should be skipped
def payment_required?
total.to_f > 0.0
end
# If true, causes the confirmation step to happen during the checkout process
def confirmation_required?
Spree::Config[:always_include_confirm_step] ||
payments.valid.map(&:payment_method).compact.any?(&:payment_profiles_supported?) ||
# Little hacky fix for #4117
# If this wasn't here, order would transition to address state on confirm failure
# because there would be no valid payments any more.
state == 'confirm'
end
# Indicates the number of items in the order
def item_count
line_items.inject(0) { |sum, li| sum + li.quantity }
end
def backordered?
shipments.any?(&:backordered?)
end
# Returns the relevant zone (if any) to be used for taxation purposes.
# Uses default tax zone unless there is a specific match
def tax_zone
Zone.match(tax_address) || Zone.default_tax
end
# Indicates whether tax should be backed out of the price calcualtions in
# cases where prices include tax but the customer is not required to pay
# taxes in that case.
def exclude_tax?
return false unless Spree::Config[:prices_inc_tax]
return tax_zone != Zone.default_tax
end
# Returns the address for taxation based on configuration
def tax_address
Spree::Config[:tax_using_ship_address] ? ship_address : bill_address
end
# Array of totals grouped by Adjustment#label. Useful for displaying line item
# adjustments on an invoice. For example, you can display tax breakout for
# cases where tax is included in price.
def line_item_adjustment_totals
Hash[self.line_item_adjustments.eligible.group_by(&:label).map do |label, adjustments|
total = adjustments.sum(&:amount)
[label, Spree::Money.new(total, { currency: currency })]
end]
end
def updater
#updater ||= OrderUpdater.new(self)
end
def update!
updater.update
end
def update_totals
updater.update_totals
end
def clone_billing_address
if bill_address and self.ship_address.nil?
self.ship_address = bill_address.clone
else
self.ship_address.attributes = bill_address.attributes.except('id', 'updated_at', 'created_at')
end
true
end
def allow_cancel?
return false unless completed? and state != 'canceled'
shipment_state.nil? || %w{ready backorder pending}.include?(shipment_state)
end
def awaiting_returns?
return_authorizations.any? { |return_authorization| return_authorization.authorized? }
end
def contents
#contents ||= Spree::OrderContents.new(self)
end
# Associates the specified user with the order.
def associate_user!(user)
self.user = user
self.email = user.email
self.created_by = user if self.created_by.blank?
if persisted?
# immediately persist the changes we just made, but don't use save since we might have an invalid address associated
self.class.unscoped.where(id: id).update_all(email: user.email, user_id: user.id, created_by_id: self.created_by_id)
end
end
# FIXME refactor this method and implement validation using validates_* utilities
def generate_order_number
record = true
while record
random = "R#{Array.new(9){rand(9)}.join}"
record = self.class.where(number: random).first
end
self.number = random if self.number.blank?
self.number
end
def shipped_shipments
shipments.shipped
end
def contains?(variant)
find_line_item_by_variant(variant).present?
end
def quantity_of(variant)
line_item = find_line_item_by_variant(variant)
line_item ? line_item.quantity : 0
end
def find_line_item_by_variant(variant)
line_items.detect { |line_item| line_item.variant_id == variant.id }
end
def ship_total
adjustments.shipping.sum(:amount)
end
# Creates new tax charges if there are any applicable rates. If prices already
# include taxes then price adjustments are created instead.
def create_tax_charge!
Spree::TaxRate.adjust(self)
end
def outstanding_balance
total - payment_total
end
def outstanding_balance?
self.outstanding_balance != 0
end
def name
if (address = bill_address || ship_address)
"#{address.firstname} #{address.lastname}"
end
end
def can_ship?
self.complete? || self.resumed? || self.awaiting_return? || self.returned?
end
def credit_cards
credit_card_ids = payments.from_credit_card.pluck(:source_id).uniq
CreditCard.where(id: credit_card_ids)
end
# Finalizes an in progress order after checkout is complete.
# Called after transition to complete state when payments will have been processed
def finalize!
touch :completed_at
# lock all adjustments (coupon promotions, etc.)
adjustments.update_all state: 'closed'
# update payment and shipment(s) states, and save
updater.update_payment_state
shipments.each do |shipment|
shipment.update!(self)
shipment.finalize!
end
updater.update_shipment_state
save
updater.run_hooks
deliver_order_confirmation_email
end
def deliver_order_confirmation_email
begin
OrderMailer.confirm_email(self.id).deliver
rescue Exception => e
logger.error("#{e.class.name}: #{e.message}")
logger.error(e.backtrace * "\n")
end
end
# Helper methods for checkout steps
def paid?
payment_state == 'paid' || payment_state == 'credit_owed'
end
def available_payment_methods
#available_payment_methods ||= (PaymentMethod.available(:front_end) + PaymentMethod.available(:both)).uniq
end
def pending_payments
payments.select(&:checkout?)
end
# processes any pending payments and must return a boolean as it's
# return value is used by the checkout state_machine to determine
# success or failure of the 'complete' event for the order
#
# Returns:
# - true if all pending_payments processed successfully
# - true if a payment failed, ie. raised a GatewayError
# which gets rescued and converted to TRUE when
# :allow_checkout_gateway_error is set to true
# - false if a payment failed, ie. raised a GatewayError
# which gets rescued and converted to FALSE when
# :allow_checkout_on_gateway_error is set to false
#
def process_payments!
if pending_payments.empty?
raise Core::GatewayError.new Spree.t(:no_pending_payments)
else
pending_payments.each do |payment|
break if payment_total >= total
payment.process!
if payment.completed?
self.payment_total += payment.amount
end
end
end
rescue Core::GatewayError => e
result = !!Spree::Config[:allow_checkout_on_gateway_error]
errors.add(:base, e.message) and return result
end
def billing_firstname
bill_address.try(:firstname)
end
def billing_lastname
bill_address.try(:lastname)
end
def products
line_items.map(&:product)
end
def variants
line_items.map(&:variant)
end
def insufficient_stock_lines
line_items.select &:insufficient_stock?
end
def merge!(order, user = nil)
order.line_items.each do |line_item|
next unless line_item.currency == currency
current_line_item = self.line_items.find_by(variant: line_item.variant)
if current_line_item
current_line_item.quantity += line_item.quantity
current_line_item.save
else
line_item.order_id = self.id
line_item.save
end
end
self.associate_user!(user) if !self.user && !user.blank?
# So that the destroy doesn't take out line items which may have been re-assigned
order.line_items.reload
order.destroy
end
(snip)
(snip)
(snip)
(snip)
(snip)
(snip)
(snip)
(snip)
end
end
shipment_decorator.rb
require 'ostruct'
module Spree
Shipment.class_eval do
def after_ship
inventory_units.each &:ship!
adjustment.finalize!
#send_shipped_email
touch :shipped_at
end
end
end
shipment.rb
require 'ostruct'
module Spree
class Shipment < ActiveRecord::Base
belongs_to :order, class_name: 'Spree::Order', touch: true
belongs_to :address, class_name: 'Spree::Address'
belongs_to :stock_location, class_name: 'Spree::StockLocation'
has_many :shipping_rates, dependent: :delete_all
has_many :shipping_methods, through: :shipping_rates
has_many :state_changes, as: :stateful
has_many :inventory_units, dependent: :delete_all
has_one :adjustment, as: :source, dependent: :destroy
after_save :ensure_correct_adjustment, :update_order
attr_accessor :special_instructions
accepts_nested_attributes_for :address
accepts_nested_attributes_for :inventory_units
make_permalink field: :number, length: 11, prefix: 'H'
scope :shipped, -> { with_state('shipped') }
scope :ready, -> { with_state('ready') }
scope :pending, -> { with_state('pending') }
scope :with_state, ->(*s) { where(state: s) }
scope :trackable, -> { where("tracking IS NOT NULL AND tracking != ''") }
# shipment state machine (see http://github.com/pluginaweek/state_machine/tree/master for details)
state_machine initial: :pending, use_transactions: false do
event :ready do
transition from: :pending, to: :ready, if: lambda { |shipment|
# Fix for #2040
shipment.determine_state(shipment.order) == 'ready'
}
end
event :pend do
transition from: :ready, to: :pending
end
event :ship do
transition from: :ready, to: :shipped
end
after_transition to: :shipped, do: :after_ship
event :cancel do
transition to: :canceled, from: [:pending, :ready]
end
after_transition to: :canceled, do: :after_cancel
event :resume do
transition from: :canceled, to: :ready, if: lambda { |shipment|
shipment.determine_state(shipment.order) == :ready
}
transition from: :canceled, to: :pending, if: lambda { |shipment|
shipment.determine_state(shipment.order) == :ready
}
transition from: :canceled, to: :pending
end
after_transition from: :canceled, to: [:pending, :ready], do: :after_resume
end
def to_param
number
end
def backordered?
inventory_units.any? { |inventory_unit| inventory_unit.backordered? }
end
def shipped=(value)
return unless value == '1' && shipped_at.nil?
self.shipped_at = Time.now
end
def shipping_method
selected_shipping_rate.try(:shipping_method) || shipping_rates.first.try(:shipping_method)
end
def add_shipping_method(shipping_method, selected = false)
shipping_rates.create(shipping_method: shipping_method, selected: selected)
end
def selected_shipping_rate
shipping_rates.where(selected: true).first
end
def selected_shipping_rate_id
selected_shipping_rate.try(:id)
end
def selected_shipping_rate_id=(id)
shipping_rates.update_all(selected: false)
shipping_rates.update(id, selected: true)
self.save!
end
def refresh_rates
return shipping_rates if shipped?
return [] unless can_get_rates?
# StockEstimator.new assigment below will replace the current shipping_method
original_shipping_method_id = shipping_method.try(:id)
self.shipping_rates = Stock::Estimator.new(order).shipping_rates(to_package)
if shipping_method
selected_rate = shipping_rates.detect { |rate|
rate.shipping_method_id == original_shipping_method_id
}
self.selected_shipping_rate_id = selected_rate.id if selected_rate
end
shipping_rates
end
def currency
order ? order.currency : Spree::Config[:currency]
end
# The adjustment amount associated with this shipment (if any.) Returns only the first adjustment to match
# the shipment but there should never really be more than one.
def cost
adjustment ? adjustment.amount : 0
end
alias_method :amount, :cost
def display_cost
Spree::Money.new(cost, { currency: currency })
end
alias_method :display_amount, :display_cost
def item_cost
line_items.map(&:amount).sum
end
def display_item_cost
Spree::Money.new(item_cost, { currency: currency })
end
def total_cost
cost + item_cost
end
def display_total_cost
Spree::Money.new(total_cost, { currency: currency })
end
def editable_by?(user)
!shipped?
end
def manifest
inventory_units.group_by(&:variant).map do |variant, units|
states = {}
units.group_by(&:state).each { |state, iu| states[state] = iu.count }
OpenStruct.new(variant: variant, quantity: units.length, states: states)
end
end
def line_items
if order.complete? and Spree::Config.track_inventory_levels
order.line_items.select { |li| !li.should_track_inventory? || inventory_units.pluck(:variant_id).include?(li.variant_id) }
else
order.line_items
end
end
def finalize!
InventoryUnit.finalize_units!(inventory_units)
manifest.each { |item| manifest_unstock(item) }
end
def after_cancel
manifest.each { |item| manifest_restock(item) }
end
def after_resume
manifest.each { |item| manifest_unstock(item) }
end
# Updates various aspects of the Shipment while bypassing any callbacks. Note that this method takes an explicit reference to the
# Order object. This is necessary because the association actually has a stale (and unsaved) copy of the Order and so it will not
# yield the correct results.
def update!(order)
old_state = state
new_state = determine_state(order)
update_column :state, new_state
after_ship if new_state == 'shipped' and old_state != 'shipped'
end
# Determines the appropriate +state+ according to the following logic:
#
# pending unless order is complete and +order.payment_state+ is +paid+
# shipped if already shipped (ie. does not change the state)
# ready all other cases
def determine_state(order)
return 'canceled' if order.canceled?
return 'pending' unless order.can_ship?
return 'pending' if inventory_units.any? &:backordered?
return 'shipped' if state == 'shipped'
order.paid? ? 'ready' : 'pending'
end
def tracking_url
#tracking_url ||= shipping_method.build_tracking_url(tracking)
end
def include?(variant)
inventory_units_for(variant).present?
end
def inventory_units_for(variant)
inventory_units.group_by(&:variant_id)[variant.id] || []
end
def to_package
package = Stock::Package.new(stock_location, order)
inventory_units.includes(:variant).each do |inventory_unit|
package.add inventory_unit.variant, 1, inventory_unit.state_name
end
package
end
def set_up_inventory(state, variant, order)
self.inventory_units.create(variant_id: variant.id, state: state, order_id: order.id)
end
private
def manifest_unstock(item)
stock_location.unstock item.variant, item.quantity, self
end
def manifest_restock(item)
if item.states["on_hand"].to_i > 0
stock_location.restock item.variant, item.states["on_hand"], self
end
if item.states["backordered"].to_i > 0
stock_location.restock_backordered item.variant, item.states["backordered"]
end
end
def description_for_shipping_charge
"#{Spree.t(:shipping)} (#{shipping_method.name})"
end
def after_ship
inventory_units.each &:ship!
adjustment.finalize!
send_shipped_email
touch :shipped_at
end
def send_shipped_email
ShipmentMailer.shipped_email(self.id).deliver
end
def ensure_correct_adjustment
if adjustment
adjustment.originator = shipping_method
adjustment.label = shipping_method.adjustment_label
adjustment.amount = selected_shipping_rate.cost if adjustment.open?
adjustment.save!
adjustment.reload
elsif selected_shipping_rate_id
shipping_method.create_adjustment shipping_method.adjustment_label, order, self, true, "open"
reload #ensure adjustment is present on later saves
end
end
def update_order
order.update!
end
def can_get_rates?
order.ship_address && order.ship_address.valid?
end
end
end

How do I update Nested Attributes in Rails without accepts_nested_attributes_for?

I'm working on a project for a class where I have a very large form with Nested Models. Here are the models that are important to the form, as well as their associations:
Course: has_many :time_blocks, has_many :tags, through: :taggings, belongs_to :institution, has_many :roles, has_many :users, through: :roles
TimeBlock: belongs_to :course
Tag: has_many :taggings
Tagging: belongs_to :tag, belongs_to :taggable_type
Institution: has_many :courses, has_many :users
Role: belongs_to :course, belongs_to :user
I am able to create the Nested Form correctly, but I can't get the Nested Models to update correctly. Here is the controller, the form is very long, but I have provided the params for the Nested Models. Note, I cleared out the values from the params, but some of the params have ID values because they exist in the db. I've also included the CoursesHelper to show the helper methods I'm using in the controller.
app/controllers/courses_controller.rb
def new
#course = current_user.courses.new
#course.institution = Institution.new
4.times { #course.tags.build }
7.times { #course.time_blocks.build }
end
def create
#course = Course.new(params[:course])
#course.institution = Institution.new(params[:institution])
filled_tags = set_tags(params[:tag])
#course.tags.build(filled_tags)
filled_time_blocks = set_time_blocks(params[:time_block])
#course.time_blocks.build(filled_time_blocks)
if #course.save
Role.create!(
user_id: current_user.id,
course_id: #course.id,
title: 'instructor'
)
redirect_to #course
else
(4 - filled_tags.count).times { #course.tags.build }
(7 - filled_time_blocks.count).times { #course.time_blocks.build }
flash.now[:errors] = #course.errors.full_messages
render :new
end
end
def edit
end
def update
filled_time_blocks = set_time_blocks(params[:time_block])
filled_time_blocks.each do |time_block|
#course.time_blocks.update_attributes(time_block)
end
filled_tags = set_tags(params[:tag])
filled_tags.each { |tag| #course.tags.update_attributes(tag) }
# #course.tags.update_attributes(filled_tags)
# #course.time_blocks.update_attributes(filled_time_blocks)
fail
if #course.update_attributes(params[:course])
redirect_to #course
else
flash.now[:errors] = #course.errors.full_messages
render :edit
end
end
app/helpers/courses_helper.rb
def set_time_blocks(entries)
result = []
days = entries[:day_of_week].reject! { |day| day.blank? }
days.each do |day|
time_block = {}
time_block[:day_of_week] = day
time_block[:start_time] = entries[day][:start_time]
time_block[:end_time] = entries[day][:end_time]
time_block[:id] = entries[day][:id]
result << time_block
end
result
end
def set_tags(entries)
[].tap do |tags|
entries.each do |entry|
tags << entry unless entry.values.all?(&:blank?)
end
end
end
def find_course
if params.include?(:id)
#course = Course.find(params[:id])
else
flash[:notice] = "Sorry, Could Not Find Course."
redirect_to current_user
end
end
TimeBlock Params
{"sun"=>{"start_time"=>"", "end_time"=>"", "id"=>""}, "mon"=>{"start_time"=>"", "end_time"=>"", "id"=>"3"}, "tue"=>{"start_time"=>"", "end_time"=>"", "id"=>"4"}, "wed"=>{"start_time"=>"", "end_time"=>"", "id"=>"5"}, "thu"=>{"start_time"=>"", "end_time"=>"", "id"=>"6"}, "fri"=>{"start_time"=>"", "end_time"=>"", "id"=>"7"}, "sat"=>{"start_time"=>"", "end_time"=>"", "id"=>""}, "day_of_week"=>[]}
Tag Params
[{"name"=>"", "id"=>"4"}, {"name"=>"", "id"=>""}, {"name"=>"", "id"=>""}, {"name"=>"", "id"=>""}]
If you cant make it work with accepts_nested_attributes_for then you'll have to write your own setter method(s) manually. Something like:
class Course < ActiveRecord::Base
def tag_attributes=(tags)
tags.each do |tag|
self.tags.build(tag)
end
end
end
The method name (tag_attributes= in my example) needs to match the key name that the tag params are listed under

Bulk insert using one model

I'm trying to create a form using textarea and a submit button that will allow users to do bulk insert. For example, the input would look like this:
0001;MR A
0002;MR B
The result would look like this:
mysql> select * from members;
+------+------+------+
| id | no | name |
+------+------+------+
| 1 | 0001 | MR A |
+------+------+------+
| 2 | 0002 | MR B |
+------+------+------+
I'm very new to Rails and I'm not sure on how to proceed with this one. Should I use attr_accessor? How do I handle failed validations in the form view? Is there any example? Thanks in advance.
Update
Based on MissingHandle's comment, I created a Scaffold and replace the Model's code with this:
class MemberBulk < ActiveRecord::Base
attr_accessor :member
def self.columns
#columsn ||= []
end
def self.column(name, sql_type = nil, default = nil, null = true)
columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null)
end
column :data, :text
validates :data, :create_members, :presence => true
def create_members
rows = self.data.split("\r\n")
#member = Array.new
rows.each_with_index { |row, i|
rows[i] = row.strip
cols = row.split(";")
p = Member.new
p.no = cols[0]
p.name = cols[1]
if p.valid?
member << p
else
p.errors.map { |k, v| errors.add(:data, "\"#{row}\" #{v}") }
end
}
end
def create_or_update
member.each { |p|
p.save
}
end
end
I know the code is far from complete, but I need to know is this the correct way to do it?
class MemberBulk < ActiveRecord::Base
#Tells Rails this is not actually tied to a database table
# or is it self.abstract_class = true
# or #abstract_class = true
# ?
abstract_class = true
# members holds array of members to be saved
# submitted_text is the data submitted in the form for a bulk update
attr_accessor :members, :submitted_text
attr_accessible :submitted_text
before_validation :build_members_from_text
def build_members_from_text
self.members = []
submitted_text.each_line("\r\n") do |member_as_text|
member_as_array = member_as_text.split(";")
self.members << Member.new(:number => member_as_array[0], :name => member_as_array[1])
end
end
def valid?
self.members.all?{ |m| m.valid? }
end
def save
self.members.all?{ |m| m.save }
end
end
class Member < ActiveRecord::Base
validates :number, :presence => true, :numericality => true
validates :name, :presence => true
end
So, in this code, members is an array that is a collection of the individual Member objects. And my thinking is that as much as possible, you want to hand off work to the Member class, as it is the class that will actually be tied to a database table, and on which you can expect standard rails model behavior. In order to accomplish this, I override two methods common to all ActiveRecord models: save and valid. A MemberBulk will only be valid if all it's members are valid and it will only count as saved if all of it's members are saved. You should probably also override the errors method to return the errors of it's underlying members, possibly with an indication of which one it is in the submitted text.
In the end I had to change from using Abstract Class to Active Model (not sure why, but it stoppped working the moment I upgrade to Rails v3.1). Here's the working code:
class MemberBulk
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
attr_accessor :input, :data
validates :input, presence: true
def initialize(attributes = {})no
attributes.each do |name, value|
send("#{name}=", value) if respond_to?("#{name}=")
end
end
def persisted?
false
end
def save
unless self.valid?
return false
end
data = Array.new
# Check for spaces
input.strip.split("\r\n").each do |i|
if i.strip.empty?
errors.add(:input, "There shouldn't be any empty lines")
end
no, nama = i.strip.split(";")
if no.nil? or nama.nil?
errors.add(:input, "#{i} doesn't have no or name")
else
no.strip!
nama.strip!
if no.empty? or nama.empty?
errors.add(:input, "#{i} doesn't have no or name")
end
end
p = Member.new(no: no, nama: nama)
if p.valid?
data << p
else
p.errors.full_messages.each do |error|
errors.add(:input, "\"#{i}\": #{error}")
end
end
end # input.strip
if errors.empty?
if data.any?
begin
data.each do |d|
d.save
end
rescue Exception => e
raise ActiveRecord::Rollback
end
else
errors.add(:input, "No data to be processed")
return false
end
else
return false
end
end # def
end

Resources