NameError: uninitialized constant Shipment with Rspec - ruby-on-rails
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
Related
undefined method `__metadata' for #<Participant:0x00000001076da378> with rails 6 / mongoid
I have the follow code that is working in rails 5. Updagrate to 6 I get the error undefined method `__metadata'. Here's the problematic code * def nature self.__metadata.key.to_s.singularize.to_sym # end * Have try to use method but it doesn't return what it does in rails 5 / mongoid. Mongoid version is '~> 7.0' Complete class code # Participant model class definition class Participant include Mongoid::Document include Mongoid::Timestamps include DryValidation field :address field :identifier field :name field :birthdate, type: Date field :sex field :profession field :phone field :email field :ownership_percentage field :contribution_amount field :category field :group field :registered_on, type: Date field :retired, type: Boolean field :retired_on, type: Date field :committee # Callbacks before_save :generate_identifier # Relations embedded_in :book, inverse_of: :shareholders embedded_in :book, inverse_of: :directors embedded_in :book, inverse_of: :employees embedded_in :book, inverse_of: :committee_members embeds_many :participant_files accepts_nested_attributes_for :participant_files, allow_destroy: true #Validations validates :name, presence: true validates :email, allow_blank: true, format: { with: /\A\S+#\S+\.\S+\z/i } validates :registered_on, presence: true, non_existent_date: true validates :birthdate, non_existent_date: true validates :retired_on, non_existent_date: true validate :registered_on_date def self.options_for(field_name) case field_name.to_sym when :category then [nil, :founders, :actives, :participants] when :sex then [nil, :male, :female] when :group then [nil, :legal, :accounting, :human_resources, :consumer, :employee, :management_and_administration, :communication_and_marketing, :ethic_and_gouvernance, :other] else [] end end def self.ordered # This should be as simple as .order_by(:retired_on.desc, :registered_on.asc) # but the registered_on parameters is never ordered correctly so I had to do this ugly thing :( self.all.sort_by{ |a| [ (a.retired_on ? a.retired_on.strftime('%Y%m%d') : 0), (a.registered_on ? a.registered_on.strftime('%Y%m%d') : 0) ].join } end def self.ordered_by_name participants = self.active.sort_by{ |p| p.name.downcase } participants += self.inactive.sort_by{ |p| p.name.downcase } participants end def self.active now = Time.now.strftime('%Y%m%d') self.all.select do |a| if a.registered_on if a.retired_on a.retired_on.strftime('%Y%m%d') >= now && a.registered_on.strftime('%Y%m%d') <= now else a.registered_on.strftime('%Y%m%d') <= now end end end end def self.inactive now = Time.now.strftime('%Y%m%d') self.all.select do|a| (a.retired_on && a.retired_on.strftime('%Y%m%d') < now) || (a.registered_on && a.registered_on.strftime('%Y%m%d') > now) end end def book self._parent end def committee_member? self.nature == :committee_member end def director? self.nature == :director end def employee? self.nature == :employee end def nature self.__metadata.key.to_s.singularize.to_sym # end def active? !retired? end def retired? self.retired_on && self.retired_on <= Time.zone.today end def shareholder? self.nature == :shareholder end def securities self.book.transactions.any_of({from: self.id}, {to: self.id}).asc(:transacted_on) end def save_files self.participant_files.each do |pf| pf.save end delete_objects_without_file end def has_shares? book.share_categories.each do |sc| return true if total_shares(sc) > 0 end false end def total_shares(share_category) total = 0 securities.each do |s| if s.share_category == share_category if s.nature == 'issued' or (s.nature == 'transfered' and self.id.to_s == s.to.to_s) total += s.quantity if s.quantity elsif s.nature == 'repurchased' or (s.nature == 'transfered' and self.id.to_s == s.from.to_s) total -= s.quantity if s.quantity end end end total end def share_class_percentage(sc) book.share_class_quantity(sc) > 0 ? self.total_shares(sc)/book.share_class_quantity(sc).to_f*100 : 0 end def acceptance_documents self.book.documents.select{|document| document.participant_id == self.id && document.nature == 'dir_accept'} end def resignation_documents self.book.documents.select{|document| document.participant_id == self.id && document.nature == 'dir_resig'} end private def existing_identifier? participant_type = self.__metadata.key.to_sym identifiers = book.send(participant_type).map{ |p| p.identifier if p.id != self.id }.compact identifiers.include? self.identifier end def generate_identifier self.identifier = self.name.parameterize if self.identifier.blank? i = 1 while existing_identifier? self.identifier = "#{self.identifier}-#{i}" i += 1 end end def registered_on_date unless registered_on.nil? || retired_on.nil? if registered_on > retired_on errors.add(:registered_on, I18n.t("mongoid.errors.models.participant.attributes.registered_on.greater_than_retired_on")) end end end def delete_objects_without_file self.participant_files.each do |pf| pf.delete if pf.pdf_file.file.nil? end end end```
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.
Restrict the chance to delete a booking just more than x hours before departure in Rails
I want users not to be able to cancel a booking just 2 hours before departure time. I don't know where can I write this restriction. Should I write it in the model or in the controller application? This is the pseudo-code I wrote so far: class CancelValidator < ActiveMOdel::Validator def validate(record) if record.date_trip.to_time < Date.now + 2 record.errors[:base] << 'error' end end end EDIT: This is all the code, but it still lets me destroy the booking.. why? class CountValidator < ActiveModel::Validator def validate(record) if (record.second || record.first) record.errors[:base]<< ' error ' end end end class DepartureValidator < ActiveModel::Validator def validate(record) if record.date_trip.to_date < Date.today record.errors[:base]<< ' error ' end end end class Reservation < ActiveRecord::Base validates_with DepartureValidator validates_with CountValidator before_destroy :ensure_deletable belongs_to :dep ,:class_name => 'Stop', :foreign_key => 'dep_id' belongs_to :arr ,:class_name => 'Stop',:foreign_key => 'arr_id' belongs_to :route belongs_to :user delegate :CountStop, :to => :route, prefix: true, :allow_nil => false delegate :city ,:to => :arr, :allow_nil => false delegate :city ,:to => :dep, :allow_nil => false def division return Reservation.select{|r| r.route_id == route_id && r.date_trip == date_trip } end def second if (class_point == 2) y=division.select{ |l| l.class_point == 2 }.count if(y+1 > route.train.second_class_seats) return true end end return false end def first if (class_point == 1) y=division.select{ |l| l.class_point == 1 }.count if(y+1 > route.train.prima_classe_seats) return true end end return false end def ensure_deletable self.date_trip.to_time < Time.now + 2 end end
Since you delete the value, you're going to want to add a callback instead. The benefit of this is that, before you go and delete the entity, you can decide to stop it outright if it fails your condition. Here's an example below. Caution: this is untested, but this should give you the gist of things. class Booking < ActiveRecord::Base before_destroy :ensure_deletable private def ensure_deletable self.date_trip.to_time < Date.now + 2 end end Remember, from the documentation: The method reference callbacks work by specifying a protected or private method available in the object...
accepts_nested_attributes_for: destroy all not included in array
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
Method to merge objects which are similar (differing by quantity)
I need help with a cart object which has_many line_items. If a line_item in the cart is created and it has the same exact attributes as a line_item which is already in the cart I just want to update the pre existing line_items quantity rather than create duplicate objects with separate quantities. I wrote a few methods in my models to try and make this work but it isn't working. Below is my code: models class LineItem < ActiveRecord::Base attr_accessible :cart_id, :product_id, :quantity, :unit_price, :product, :cart, :color_id, :size_id, :extra_id belongs_to :cart belongs_to :product belongs_to :color belongs_to :size belongs_to :extra validates :quantity, :presence => true def update_quantity(qty) quantity += qty quantity.save end def exists_in_collect?(items) if items.include?(product) if color == items.color && size == items.sizes && extra == items.extra return true end else return false end end end class Cart < ActiveRecord::Base attr_accessible :purchased_at has_many :line_items has_one :order def where_line_item_with(prod_id) line_items.where(:product_id => prod_id) end end controller class LineItemsController < ApplicationController def new #line_item = LineItem.new end def create #line_item = LineItem.new(params[:line_item].merge(:cart => current_cart)) if #line_item.exists_in_collect?(current_cart.line_items) current_cart.where_line_item_with(product.id).update_quantity(#line_item.quantity) #line_item.destroy! else #line_item.save! #line_item.update_attributes!(:unit_price => #line_item.item_price) end redirect_to current_cart_url end def update #line_item = LineItem.find(params[:id]) #line_item.update_attributes(params[:line_item]) redirect_to current_cart_url end Any insight is fully appreciated.
1.You should change your where_line_item_with(prod_id) to the following: def where_line_item_with(prod_id) line_items.where(:product_id => prod_id).first end As the where returns an array and you cannot do update_quantity(#line_item.quantity) on an array. 2.In exists_in_collect?(items) - Here your aim is to find if the items of the cart include the item similar to new item. it should be updated as following: def exists_in_collect?(items) items.each do |item| if color == item.color && size == item.sizes && extra == item.extra && product == item.product return true end end return false end