diff --git a/lib/spree/core/calculated_adjustments.rb b/lib/spree/core/calculated_adjustments.rb new file mode 100644 index 0000000000..221a9926f0 --- /dev/null +++ b/lib/spree/core/calculated_adjustments.rb @@ -0,0 +1,75 @@ +module Spree + module Core + module CalculatedAdjustments + def self.included(klass) + klass.class_eval do + has_one :calculator, :class_name => "Spree::Calculator", :as => :calculable, :dependent => :destroy + accepts_nested_attributes_for :calculator + validates :calculator, :presence => true + + def self.calculators + spree_calculators.send model_name_without_spree_namespace + end + + def calculator_type + calculator.class.to_s if calculator + end + + def calculator_type=(calculator_type) + klass = calculator_type.constantize if calculator_type + self.calculator = klass.new if klass && !self.calculator.is_a?(klass) + end + + # Creates a new adjustment for the target object (which is any class that has_many :adjustments) and + # sets amount based on the calculator as applied to the calculable argument (Order, LineItems[], Shipment, etc.) + # By default the adjustment will not be considered mandatory + def create_adjustment(label, target, calculable, mandatory=false, state="closed") + # Adjustment calculations done on Spree::Shipment objects MUST + # be done on their to_package'd variants instead + # It's only the package that contains the correct information. + # See https://github.com/spree/spree_active_shipping/pull/96 et. al + old_calculable = calculable + calculable = calculable.to_package if calculable.is_a?(Spree::Shipment) + amount = compute_amount(calculable) + return if amount == 0 && !mandatory + target.adjustments.create( + :amount => amount, + :source => old_calculable, + :originator => self, + :label => label, + :mandatory => mandatory, + :state => state + ) + end + + # Updates the amount of the adjustment using our Calculator and calling the +compute+ method with the +calculable+ + # referenced passed to the method. + def update_adjustment(adjustment, calculable) + # Adjustment calculations done on Spree::Shipment objects MUST + # be done on their to_package'd variants instead + # It's only the package that contains the correct information. + # See https://github.com/spree/spree_active_shipping/pull/96 et. al + calculable = calculable.to_package if calculable.is_a?(Spree::Shipment) + adjustment.update_column(:amount, compute_amount(calculable)) + end + + # Calculate the amount to be used when creating an adjustment + # NOTE: May be overriden by classes where this module is included into. + # Such as Spree::Promotion::Action::CreateAdjustment. + def compute_amount(calculable) + self.calculator.compute(calculable) + end + + private + def self.model_name_without_spree_namespace + self.to_s.tableize.gsub('/', '_').sub('spree_', '') + end + + def self.spree_calculators + Rails.application.config.spree.calculators + end + end + end + end + end +end diff --git a/lib/spree/core/delegate_belongs_to.rb b/lib/spree/core/delegate_belongs_to.rb new file mode 100644 index 0000000000..2adb978557 --- /dev/null +++ b/lib/spree/core/delegate_belongs_to.rb @@ -0,0 +1,89 @@ +## +# Creates methods on object which delegate to an association proxy. +# see delegate_belongs_to for two uses +# +# Todo - integrate with ActiveRecord::Dirty to make sure changes to delegate object are noticed +# Should do +# class User < ActiveRecord::Base; delegate_belongs_to :contact, :firstname; end +# class Contact < ActiveRecord::Base; end +# u = User.first +# u.changed? # => false +# u.firstname = 'Bobby' +# u.changed? # => true +# +# Right now the second call to changed? would return false +# +# Todo - add has_one support. fairly straightforward addition +## +module DelegateBelongsTo + extend ActiveSupport::Concern + + module ClassMethods + + @@default_rejected_delegate_columns = ['created_at','created_on','updated_at','updated_on','lock_version','type','id','position','parent_id','lft','rgt'] + mattr_accessor :default_rejected_delegate_columns + + ## + # Creates methods for accessing and setting attributes on an association. Uses same + # default list of attributes as delegates_to_association. + # @todo Integrate this with ActiveRecord::Dirty, so if you set a property through one of these setters and then call save on this object, it will save the associated object automatically. + # delegate_belongs_to :contact + # delegate_belongs_to :contact, [:defaults] ## same as above, and useless + # delegate_belongs_to :contact, [:defaults, :address, :fullname], :class_name => 'VCard' + ## + def delegate_belongs_to(association, *attrs) + opts = attrs.extract_options! + initialize_association :belongs_to, association, opts + attrs = get_association_column_names(association) if attrs.empty? + attrs.concat get_association_column_names(association) if attrs.delete :defaults + attrs.each do |attr| + class_def attr do |*args| + if args.empty? + send(:delegator_for, association).send(attr) + else + send(:delegator_for, association).send(attr, *args) + end + end + class_def "#{attr}=" do |val| + send(:delegator_for, association).send("#{attr}=", val) + end + end + end + + protected + + def get_association_column_names(association, without_default_rejected_delegate_columns=true) + begin + association_klass = reflect_on_association(association).klass + methods = association_klass.column_names + methods.reject!{|x|default_rejected_delegate_columns.include?(x.to_s)} if without_default_rejected_delegate_columns + return methods + rescue + return [] + end + end + + ## + # initialize_association :belongs_to, :contact + def initialize_association(type, association, opts={}) + raise 'Illegal or unimplemented association type.' unless [:belongs_to].include?(type.to_s.to_sym) + send type, association, opts if reflect_on_association(association).nil? + end + + private + + def class_def(name, method=nil, &blk) + class_eval { method.nil? ? define_method(name, &blk) : define_method(name, method) } + end + + end + + def delegator_for(association) + send("#{association}=", self.class.reflect_on_association(association).klass.new) if send(association).nil? + send(association) + end + protected :delegator_for + +end + +ActiveRecord::Base.send :include, DelegateBelongsTo diff --git a/lib/spree/core/gateway_error.rb b/lib/spree/core/gateway_error.rb new file mode 100644 index 0000000000..5a6ddda30b --- /dev/null +++ b/lib/spree/core/gateway_error.rb @@ -0,0 +1,5 @@ +module Spree + module Core + class GatewayError < RuntimeError; end + end +end diff --git a/lib/spree/core/mail_interceptor.rb b/lib/spree/core/mail_interceptor.rb new file mode 100644 index 0000000000..e0a1400ab8 --- /dev/null +++ b/lib/spree/core/mail_interceptor.rb @@ -0,0 +1,22 @@ +# Allows us to intercept any outbound mail message and make last minute changes +# (such as specifying a "from" address or sending to a test email account) +# +# See http://railscasts.com/episodes/206-action-mailer-in-rails-3 for more details. +module Spree + module Core + class MailInterceptor + def self.delivering_email(message) + return unless MailSettings.override? + + if Config[:intercept_email].present? + message.subject = "#{message.to} #{message.subject}" + message.to = Config[:intercept_email] + end + + if Config[:mail_bcc].present? + message.bcc ||= Config[:mail_bcc] + end + end + end + end +end diff --git a/lib/spree/core/mail_settings.rb b/lib/spree/core/mail_settings.rb new file mode 100644 index 0000000000..757144af8e --- /dev/null +++ b/lib/spree/core/mail_settings.rb @@ -0,0 +1,60 @@ +module Spree + module Core + class MailSettings + MAIL_AUTH = ['None', 'plain', 'login', 'cram_md5'] + SECURE_CONNECTION_TYPES = ['None','SSL','TLS'] + + # Override the Rails application mail settings based on preferences + # This makes it possible to configure the mail settings through an admin + # interface instead of requiring changes to the Rails envrionment file + def self.init + self.new.override! if override? + end + + def self.override? + Config.override_actionmailer_config + end + + def override! + if Config.enable_mail_delivery + ActionMailer::Base.default_url_options[:host] ||= Config.site_url + ActionMailer::Base.smtp_settings = mail_server_settings + ActionMailer::Base.perform_deliveries = true + else + ActionMailer::Base.perform_deliveries = false + end + end + + private + def mail_server_settings + settings = if need_authentication? + basic_settings.merge(user_credentials) + else + basic_settings + end + + settings.merge :enable_starttls_auto => secure_connection? + end + + def user_credentials + { :user_name => Config.smtp_username, + :password => Config.smtp_password } + end + + def basic_settings + { :address => Config.mail_host, + :domain => Config.mail_domain, + :port => Config.mail_port, + :authentication => Config.mail_auth_type } + end + + def need_authentication? + Config.mail_auth_type != 'None' + end + + def secure_connection? + Config.secure_connection_type == 'TLS' + end + end + end +end diff --git a/lib/spree/core/permalinks.rb b/lib/spree/core/permalinks.rb new file mode 100644 index 0000000000..6e0fcc8735 --- /dev/null +++ b/lib/spree/core/permalinks.rb @@ -0,0 +1,71 @@ +require 'stringex' + +module Spree + module Core + module Permalinks + extend ActiveSupport::Concern + + included do + class_attribute :permalink_options + end + + module ClassMethods + def make_permalink(options={}) + options[:field] ||= :permalink + self.permalink_options = options + + if self.connected? + if self.table_exists? && self.column_names.include?(permalink_options[:field].to_s) + before_validation(:on => :create) { save_permalink } + end + end + end + + def find_by_param(value, *args) + self.send("find_by_#{permalink_field}", value, *args) + end + + def find_by_param!(value, *args) + self.send("find_by_#{permalink_field}!", value, *args) + end + + def permalink_field + permalink_options[:field] + end + + def permalink_prefix + permalink_options[:prefix] || "" + end + + def permalink_order + order = permalink_options[:order] + "#{order} ASC," if order + end + end + + def generate_permalink + "#{self.class.permalink_prefix}#{Array.new(9){rand(9)}.join}" + end + + def save_permalink(permalink_value=self.to_param) + self.with_lock do + permalink_value ||= generate_permalink + + field = self.class.permalink_field + # Do other links exist with this permalink? + other = self.class.where("#{self.class.table_name}.#{field} LIKE ?", "#{permalink_value}%") + if other.any? + # Find the existing permalink with the highest number, and increment that number. + # (If none of the existing permalinks have a number, this will evaluate to 1.) + number = other.map { |o| o.send(field)[/-(\d+)$/, 1].to_i }.max + 1 + permalink_value += "-#{number.to_s}" + end + write_attribute(field, permalink_value) + end + end + end + end +end + +ActiveRecord::Base.send :include, Spree::Core::Permalinks +ActiveRecord::Relation.send :include, Spree::Core::Permalinks diff --git a/lib/spree/core/s3_support.rb b/lib/spree/core/s3_support.rb new file mode 100644 index 0000000000..8ae8354391 --- /dev/null +++ b/lib/spree/core/s3_support.rb @@ -0,0 +1,25 @@ +module Spree + module Core + # This module exists to reduce duplication in S3 settings between + # the Image and Taxon models in Spree + module S3Support + extend ActiveSupport::Concern + + included do + def self.supports_s3(field) + # Load user defined paperclip settings + config = Spree::Config + if config[:use_s3] + s3_creds = { :access_key_id => config[:s3_access_key], :secret_access_key => config[:s3_secret], :bucket => config[:s3_bucket] } + self.attachment_definitions[field][:storage] = :s3 + self.attachment_definitions[field][:s3_credentials] = s3_creds + self.attachment_definitions[field][:s3_headers] = ActiveSupport::JSON.decode(config[:s3_headers]) + self.attachment_definitions[field][:bucket] = config[:s3_bucket] + self.attachment_definitions[field][:s3_protocol] = config[:s3_protocol].downcase unless config[:s3_protocol].blank? + self.attachment_definitions[field][:s3_host_alias] = config[:s3_host_alias] unless config[:s3_host_alias].blank? + end + end + end + end + end +end diff --git a/lib/spree/core/token_resource.rb b/lib/spree/core/token_resource.rb new file mode 100644 index 0000000000..48697f29b2 --- /dev/null +++ b/lib/spree/core/token_resource.rb @@ -0,0 +1,27 @@ +module Spree + module Core + module TokenResource + module ClassMethods + def token_resource + has_one :tokenized_permission, :as => :permissable + delegate :token, :to => :tokenized_permission, :allow_nil => true + after_create :create_token + end + end + + def create_token + permission = build_tokenized_permission + permission.token = token = ::SecureRandom::hex(8) + permission.save! + token + end + + def self.included(receiver) + receiver.extend ClassMethods + end + end + end +end + +ActiveRecord::Base.class_eval { include Spree::Core::TokenResource } + diff --git a/lib/spree/product_duplicator.rb b/lib/spree/product_duplicator.rb new file mode 100644 index 0000000000..f563223f70 --- /dev/null +++ b/lib/spree/product_duplicator.rb @@ -0,0 +1,61 @@ +module Spree + class ProductDuplicator + attr_accessor :product + + def initialize(product) + @product = product + end + + def duplicate + new_product = duplicate_product + + # don't dup the actual variants, just the characterising types + new_product.option_types = product.option_types if product.has_variants? + + # allow site to do some customization + new_product.send(:duplicate_extra, product) if new_product.respond_to?(:duplicate_extra) + new_product.save! + new_product + end + + protected + + def duplicate_product + product.dup.tap do |new_product| + new_product.name = "COPY OF #{product.name}" + new_product.taxons = product.taxons + new_product.created_at = nil + new_product.deleted_at = nil + new_product.updated_at = nil + new_product.product_properties = reset_properties + new_product.master = duplicate_master + end + end + + def duplicate_master + master = product.master + master.dup.tap do |new_master| + new_master.sku = "COPY OF #{master.sku}" + new_master.deleted_at = nil + new_master.images = master.images.map { |image| duplicate_image image } + new_master.price = master.price + new_master.currency = master.currency + end + end + + def duplicate_image(image) + new_image = image.dup + new_image.assign_attributes(:attachment => image.attachment.clone) + new_image + end + + def reset_properties + product.product_properties.map do |prop| + prop.dup.tap do |new_prop| + new_prop.created_at = nil + new_prop.updated_at = nil + end + end + end + end +end diff --git a/spec/lib/spree/core/calculated_adjustments_spec.rb b/spec/lib/spree/core/calculated_adjustments_spec.rb new file mode 100644 index 0000000000..eecfee445f --- /dev/null +++ b/spec/lib/spree/core/calculated_adjustments_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper' + +# Its pretty difficult to test this module in isolation b/c it needs to work in conjunction with an actual class that +# extends ActiveRecord::Base and has a corresponding table in the database. So we'll just test it using Order and +# ShippingMethod instead since those classes are including the module. +describe Spree::Core::CalculatedAdjustments do + + let(:calculator) { mock_model(Spree::Calculator, :compute => 10, :[]= => nil) } + + it "should add has_one :calculator relationship" do + assert Spree::ShippingMethod.reflect_on_all_associations(:has_one).map(&:name).include?(:calculator) + end + + let(:tax_rate) { Spree::TaxRate.new(:calculator => calculator) } + + context "#create_adjustment and its resulting adjustment" do + let(:order) { Spree::Order.create } + let(:target) { order } + + it "should be associated with the target" do + target.adjustments.should_receive(:create) + tax_rate.create_adjustment("foo", target, order) + end + + it "should have the correct originator and an amount derived from the calculator and supplied calculable" do + adjustment = tax_rate.create_adjustment("foo", target, order) + adjustment.should_not be_nil + adjustment.amount.should == 10 + adjustment.source.should == order + adjustment.originator.should == tax_rate + end + + it "should be mandatory if true is supplied for that parameter" do + adjustment = tax_rate.create_adjustment("foo", target, order, true) + adjustment.should be_mandatory + end + + context "when the calculator returns 0" do + before { calculator.stub :compute => 0 } + + context "when adjustment is mandatory" do + before { tax_rate.create_adjustment("foo", target, order, true) } + + it "should create an adjustment" do + Spree::Adjustment.count.should == 1 + end + end + + context "when adjustment is not mandatory" do + before { tax_rate.create_adjustment("foo", target, order, false) } + + it "should not create an adjustment" do + Spree::Adjustment.count.should == 0 + end + end + end + + end + + context "#update_adjustment" do + it "should update the adjustment using its calculator (and the specified source)" do + adjustment = double(:adjustment).as_null_object + calculable = double :calculable + adjustment.should_receive(:update_column).with(:amount, 10) + tax_rate.update_adjustment(adjustment, calculable) + end + end + +end diff --git a/spec/lib/spree/core/mail_interceptor_spec.rb b/spec/lib/spree/core/mail_interceptor_spec.rb new file mode 100644 index 0000000000..9375a0471c --- /dev/null +++ b/spec/lib/spree/core/mail_interceptor_spec.rb @@ -0,0 +1,78 @@ +require 'spec_helper' + +# We'll use the OrderMailer as a quick and easy way to test. IF it works here +# it works for all email (in theory.) +describe Spree::OrderMailer do + let(:order) { Spree::Order.new(:email => "customer@example.com") } + let(:message) { Spree::OrderMailer.confirm_email(order) } + + before(:all) do + ActionMailer::Base.perform_deliveries = true + ActionMailer::Base.deliveries.clear + end + + context "#deliver" do + before do + ActionMailer::Base.delivery_method = :test + end + + after { ActionMailer::Base.deliveries.clear } + + it "should use the from address specified in the preference" do + Spree::Config[:mails_from] = "no-reply@foobar.com" + message.deliver + @email = ActionMailer::Base.deliveries.first + @email.from.should == ["no-reply@foobar.com"] + end + + it "should use the provided from address" do + Spree::Config[:mails_from] = "preference@foobar.com" + message.from = "override@foobar.com" + message.to = "test@test.com" + message.deliver + email = ActionMailer::Base.deliveries.first + email.from.should == ["override@foobar.com"] + email.to.should == ["test@test.com"] + end + + it "should add the bcc email when provided" do + Spree::Config[:mail_bcc] = "bcc-foo@foobar.com" + message.deliver + @email = ActionMailer::Base.deliveries.first + @email.bcc.should == ["bcc-foo@foobar.com"] + end + + context "when intercept_email is provided" do + it "should strip the bcc recipients" do + message.bcc.should be_blank + end + + it "should strip the cc recipients" do + message.cc.should be_blank + end + + it "should replace the receipient with the specified address" do + Spree::Config[:intercept_email] = "intercept@foobar.com" + message.deliver + @email = ActionMailer::Base.deliveries.first + @email.to.should == ["intercept@foobar.com"] + end + + it "should modify the subject to include the original email" do + Spree::Config[:intercept_email] = "intercept@foobar.com" + message.deliver + @email = ActionMailer::Base.deliveries.first + @email.subject.match(/customer@example\.com/).should be_true + end + end + + context "when intercept_mode is not provided" do + it "should not modify the recipient" do + Spree::Config[:intercept_email] = "" + message.deliver + @email = ActionMailer::Base.deliveries.first + @email.to.should == ["customer@example.com"] + end + end + end +end diff --git a/spec/lib/spree/core/mail_settings_spec.rb b/spec/lib/spree/core/mail_settings_spec.rb new file mode 100644 index 0000000000..8528a6226d --- /dev/null +++ b/spec/lib/spree/core/mail_settings_spec.rb @@ -0,0 +1,89 @@ +require 'spec_helper' + +module Spree + module Core + describe MailSettings do + let!(:subject) { MailSettings.new } + + context "override option is true" do + before { Config.override_actionmailer_config = true } + + context "init" do + it "calls override!" do + MailSettings.should_receive(:new).and_return(subject) + subject.should_receive(:override!) + MailSettings.init + end + end + + context "enable delivery" do + before { Config.enable_mail_delivery = true } + + context "overrides appplication defaults" do + + context "authentication method is none" do + before do + Config.mail_host = "smtp.example.com" + Config.mail_domain = "example.com" + Config.mail_port = 123 + Config.mail_auth_type = MailSettings::SECURE_CONNECTION_TYPES[0] + Config.smtp_username = "schof" + Config.smtp_password = "hellospree!" + Config.secure_connection_type = "TLS" + subject.override! + end + + it { ActionMailer::Base.smtp_settings[:address].should == "smtp.example.com" } + it { ActionMailer::Base.smtp_settings[:domain].should == "example.com" } + it { ActionMailer::Base.smtp_settings[:port].should == 123 } + it { ActionMailer::Base.smtp_settings[:authentication].should == "None" } + it { ActionMailer::Base.smtp_settings[:enable_starttls_auto].should be_true } + + it "doesnt touch user name config" do + ActionMailer::Base.smtp_settings[:user_name].should == nil + end + + it "doesnt touch password config" do + ActionMailer::Base.smtp_settings[:password].should == nil + end + end + end + + context "when mail_auth_type is other than none" do + before do + Config.mail_auth_type = "login" + Config.smtp_username = "schof" + Config.smtp_password = "hellospree!" + subject.override! + end + + context "overrides user credentials" do + it { ActionMailer::Base.smtp_settings[:user_name].should == "schof" } + it { ActionMailer::Base.smtp_settings[:password].should == "hellospree!" } + end + end + end + + context "do not enable delivery" do + before do + Config.enable_mail_delivery = false + subject.override! + end + + it { ActionMailer::Base.perform_deliveries.should be_false } + end + end + + context "override option is false" do + before { Config.override_actionmailer_config = false } + + context "init" do + it "doesnt calls override!" do + subject.should_not_receive(:override!) + MailSettings.init + end + end + end + end + end +end diff --git a/spec/lib/spree/core/token_resource_spec.rb b/spec/lib/spree/core/token_resource_spec.rb new file mode 100644 index 0000000000..40ac8f838a --- /dev/null +++ b/spec/lib/spree/core/token_resource_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +# Its pretty difficult to test this module in isolation b/c it needs to work in conjunction with an actual class that +# extends ActiveRecord::Base and has a corresponding table in the database. So we'll just test it using Order instead +# since those classes are including the module. +describe Spree::Core::TokenResource do + let(:order) { Spree::Order.new } + let(:permission) { mock_model(Spree::TokenizedPermission) } + + it 'should add has_one :tokenized_permission relationship' do + assert Spree::Order.reflect_on_all_associations(:has_one).map(&:name).include?(:tokenized_permission) + end + + context '#token' do + it 'should return the token of the associated permission' do + order.stub :tokenized_permission => permission + permission.stub :token => 'foo' + order.token.should == 'foo' + end + + it 'should return nil if there is no associated permission' do + order.token.should be_nil + end + end + + context '#create_token' do + it 'should create a randomized 16 character token' do + token = order.create_token + token.size.should == 16 + end + end +end diff --git a/spec/lib/spree/product_duplicator_spec.rb b/spec/lib/spree/product_duplicator_spec.rb new file mode 100644 index 0000000000..e9fef3ae07 --- /dev/null +++ b/spec/lib/spree/product_duplicator_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +module Spree + describe Spree::ProductDuplicator do + let(:product) do + double 'Product', + :name => "foo", + :taxons => [], + :product_properties => [property], + :master => variant, + :has_variants? => false + end + + let(:new_product) do + double 'New Product', + :save! => true + end + + let(:property) do + double 'Property' + end + + let(:new_property) do + double 'New Property' + end + + let(:variant) do + double 'Variant', + :sku => "12345", + :price => 19.99, + :currency => "AUD", + :images => [image] + end + + let(:new_variant) do + double 'New Variant', + :sku => "12345" + end + + let(:image) do + double 'Image', + :attachment => double('Attachment') + end + + let(:new_image) do + double 'New Image' + end + + + before do + product.should_receive(:dup).and_return(new_product) + variant.should_receive(:dup).and_return(new_variant) + image.should_receive(:dup).and_return(new_image) + property.should_receive(:dup).and_return(new_property) + end + + it "can duplicate a product" do + duplicator = Spree::ProductDuplicator.new(product) + new_product.should_receive(:name=).with("COPY OF foo") + new_product.should_receive(:taxons=).with([]) + new_product.should_receive(:product_properties=).with([new_property]) + new_product.should_receive(:created_at=).with(nil) + new_product.should_receive(:updated_at=).with(nil) + new_product.should_receive(:deleted_at=).with(nil) + new_product.should_receive(:master=).with(new_variant) + + new_variant.should_receive(:sku=).with("COPY OF 12345") + new_variant.should_receive(:deleted_at=).with(nil) + new_variant.should_receive(:images=).with([new_image]) + new_variant.should_receive(:price=).with(variant.price) + new_variant.should_receive(:currency=).with(variant.currency) + + image.attachment.should_receive(:clone).and_return(image.attachment) + + new_image.should_receive(:assign_attributes). + with(:attachment => image.attachment). + and_return(new_image) + + new_property.should_receive(:created_at=).with(nil) + new_property.should_receive(:updated_at=).with(nil) + + duplicator.duplicate + end + + end +end +