diff --git a/app/assets/images/ofn-logo.png b/app/assets/images/ofn-logo.png new file mode 100644 index 0000000000..6058b26a75 Binary files /dev/null and b/app/assets/images/ofn-logo.png differ diff --git a/app/models/spree/app_configuration.rb b/app/models/spree/app_configuration.rb new file mode 100644 index 0000000000..d56ca5a6ac --- /dev/null +++ b/app/models/spree/app_configuration.rb @@ -0,0 +1,148 @@ +# frozen_string_literal: true + +# This is the primary location for defining spree preferences +# +# This file allows us to add global configuration variables, which +# we can allow to be modified in the UI by adding appropriate form +# elements to existing or new configuration pages. +# +# The expectation is that this is created once and stored in +# the spree environment +# +# setters: +# a.color = :blue +# a[:color] = :blue +# a.set :color = :blue +# a.preferred_color = :blue +# +# getters: +# a.color +# a[:color] +# a.get :color +# a.preferred_color +# +module Spree + class AppConfiguration < Preferences::Configuration + # Should state/state_name be required + preference :address_requires_state, :boolean, default: true + preference :admin_interface_logo, :string, default: 'ofn-logo.png' + preference :admin_products_per_page, :integer, default: 10 + # Should only be true if you don't need to track inventory + preference :allow_backorder_shipping, :boolean, default: false + preference :allow_checkout_on_gateway_error, :boolean, default: false + preference :allow_guest_checkout, :boolean, default: true + preference :allow_ssl_in_development_and_test, :boolean, default: false + preference :allow_ssl_in_production, :boolean, default: true + preference :allow_ssl_in_staging, :boolean, default: true + # Automatically capture the credit card (as opposed to just authorize and capture later) + preference :auto_capture, :boolean, default: false + # Replace with the name of a zone if you would like to limit the countries + preference :checkout_zone, :string, default: nil + preference :currency, :string, default: "USD" + preference :currency_decimal_mark, :string, default: "." + preference :currency_symbol_position, :string, default: "before" + preference :currency_thousands_separator, :string, default: "," + preference :display_currency, :boolean, default: false + preference :default_country_id, :integer + preference :default_meta_description, :string, default: 'OFN demo site' + preference :default_meta_keywords, :string, default: 'ofn, demo' + preference :default_seo_title, :string, default: '' + preference :hide_cents, :boolean, default: false + preference :layout, :string, default: 'darkswarm' + preference :logo, :string, default: 'ofn-logo.png' + + # Maximum nesting level in taxons menu + preference :max_level_in_taxons_menu, :integer, default: 1 + preference :orders_per_page, :integer, default: 15 + preference :prices_inc_tax, :boolean, default: false + preference :products_per_page, :integer, default: 12 + preference :redirect_https_to_http, :boolean, default: false + preference :require_master_price, :boolean, default: true + preference :shipment_inc_vat, :boolean, default: false + # Request instructions/info for shipping + preference :shipping_instructions, :boolean, default: false + # Displays variant full price or difference with product price. + preference :show_variant_full_price, :boolean, default: false + preference :show_products_without_price, :boolean, default: false + preference :show_raw_product_description, :boolean, default: false + preference :site_name, :string, default: 'OFN Demo Site' + preference :site_url, :string, default: 'demo.openfoodnetwork.org' + preference :tax_using_ship_address, :boolean, default: true + # Determines whether to track on_hand values for variants / products. + preference :track_inventory_levels, :boolean, default: true + + # Preferences related to image settings + preference :attachment_default_url, :string, + default: '/spree/products/:id/:style/:basename.:extension' + preference :attachment_path, :string, + default: ':rails_root/public/spree/products/:id/:style/:basename.:extension' + preference :attachment_url, :string, + default: '/spree/products/:id/:style/:basename.:extension' + preference :attachment_styles, :string, + default: "{\"mini\":\"48x48>\",\"small\":\"100x100>\",\"product\":\"240x240>\",\"large\":\"600x600>\"}" + preference :attachment_default_style, :string, default: 'product' + preference :s3_access_key, :string + preference :s3_bucket, :string + preference :s3_secret, :string + preference :s3_headers, :string, default: "{\"Cache-Control\":\"max-age=31557600\"}" + preference :use_s3, :boolean, default: false # Use S3 for images rather than the file system + preference :s3_protocol, :string + preference :s3_host_alias, :string + + # Default mail headers settings + preference :enable_mail_delivery, :boolean, default: false + preference :mails_from, :string, default: 'ofn@example.com' + preference :mail_bcc, :string, default: 'ofn@example.com' + preference :intercept_email, :string, default: nil + + # Default smtp settings + preference :override_actionmailer_config, :boolean, default: true + preference :mail_host, :string, default: 'localhost' + preference :mail_domain, :string, default: 'localhost' + preference :mail_port, :integer, default: 25 + preference :secure_connection_type, :string, + default: Core::MailSettings::SECURE_CONNECTION_TYPES[0] + preference :mail_auth_type, :string, default: Core::MailSettings::MAIL_AUTH[0] + preference :smtp_username, :string + preference :smtp_password, :string + + # Embedded Shopfronts + preference :enable_embedded_shopfronts, :boolean, default: false + preference :embedded_shopfronts_whitelist, :text, default: nil + + # Legal Preferences + preference :footer_tos_url, :string, default: "/Terms-of-service.pdf" + preference :enterprises_require_tos, :boolean, default: false + preference :privacy_policy_url, :string, default: nil + preference :cookies_consent_banner_toggle, :boolean, default: false + preference :cookies_policy_matomo_section, :boolean, default: false + + # Tax Preferences + preference :products_require_tax_category, :boolean, default: false + preference :shipping_tax_rate, :decimal, default: 0 + + # Monitoring + preference :last_job_queue_heartbeat_at, :string, default: nil + + # External services + preference :bugherd_api_key, :string, default: nil + preference :matomo_url, :string, default: nil + preference :matomo_site_id, :string, default: nil + preference :matomo_tag_manager_url, :string, default: nil + + # Invoices & Receipts + preference :enable_invoices?, :boolean, default: true + preference :invoice_style2?, :boolean, default: false + preference :enable_receipt_printing?, :boolean, default: false + + # Stripe Connect + preference :stripe_connect_enabled, :boolean, default: false + + # Number localization + preference :enable_localized_number?, :boolean, default: false + + # Enable cache + preference :enable_products_cache?, :boolean, + default: (Rails.env.production? || Rails.env.staging?) + end +end diff --git a/app/models/spree/app_configuration_decorator.rb b/app/models/spree/app_configuration_decorator.rb deleted file mode 100644 index a039ddbd45..0000000000 --- a/app/models/spree/app_configuration_decorator.rb +++ /dev/null @@ -1,44 +0,0 @@ -Spree::AppConfiguration.class_eval do - # This file decorates the existing preferences file defined by Spree. - # It allows us to add our own global configuration variables, which - # we can allow to be modified in the UI by adding appropriate form - # elements to existing or new configuration pages. - - # Embedded Shopfronts - preference :enable_embedded_shopfronts, :boolean, default: false - preference :embedded_shopfronts_whitelist, :text, default: nil - - # Legal Preferences - preference :footer_tos_url, :string, default: "/Terms-of-service.pdf" - preference :enterprises_require_tos, :boolean, default: false - preference :privacy_policy_url, :string, default: nil - preference :cookies_consent_banner_toggle, :boolean, default: false - preference :cookies_policy_matomo_section, :boolean, default: false - - # Tax Preferences - preference :products_require_tax_category, :boolean, default: false - preference :shipping_tax_rate, :decimal, default: 0 - - # Monitoring - preference :last_job_queue_heartbeat_at, :string, default: nil - - # External services - preference :bugherd_api_key, :string, default: nil - preference :matomo_url, :string, default: nil - preference :matomo_site_id, :string, default: nil - preference :matomo_tag_manager_url, :string, default: nil - - # Invoices & Receipts - preference :enable_invoices?, :boolean, default: true - preference :invoice_style2?, :boolean, default: false - preference :enable_receipt_printing?, :boolean, default: false - - # Stripe Connect - preference :stripe_connect_enabled, :boolean, default: false - - # Number localization - preference :enable_localized_number?, :boolean, default: false - - # Enable cache - preference :enable_products_cache?, :boolean, default: (Rails.env.production? || Rails.env.staging?) -end diff --git a/app/models/spree/preference.rb b/app/models/spree/preference.rb new file mode 100644 index 0000000000..b877f460ce --- /dev/null +++ b/app/models/spree/preference.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Spree + class Preference < ActiveRecord::Base + serialize :value + + validates :key, presence: true + validates :value_type, presence: true + + scope :valid, -> { + where(Spree::Preference.arel_table[:key].not_eq(nil)). + where(Spree::Preference.arel_table[:value_type].not_eq(nil)) + } + + # The type conversions here should match + # the ones in spree::preferences::preferrable#convert_preference_value + def value + if self[:value_type].present? + case self[:value_type].to_sym + when :string, :text + self[:value].to_s + when :password + self[:value].to_s + when :decimal + BigDecimal(self[:value].to_s).round(2, BigDecimal::ROUND_HALF_UP) + when :integer + self[:value].to_i + when :boolean + !(self[:value].to_s =~ /^[t|1]/i).nil? + else + self[:value].is_a?(String) ? YAML.safe_load(self[:value]) : self[:value] + end + else + self[:value] + end + end + + def raw_value + self[:value] + end + end +end diff --git a/app/models/spree/preferences/configuration.rb b/app/models/spree/preferences/configuration.rb new file mode 100644 index 0000000000..d1a514484d --- /dev/null +++ b/app/models/spree/preferences/configuration.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +# This takes the preferrable methods and adds some +# syntatic sugar to access the preferences +# +# class App < Configuration +# preference :color, :string +# end +# +# a = App.new +# +# setters: +# a.color = :blue +# a[:color] = :blue +# a.set :color = :blue +# a.preferred_color = :blue +# +# getters: +# a.color +# a[:color] +# a.get :color +# a.preferred_color +# +# +module Spree + module Preferences + class Configuration + include Spree::Preferences::Preferable + + def configure + yield(self) if block_given? + end + + def preference_cache_key(name) + [ENV['RAILS_CACHE_ID'], self.class.name, name].flatten.join('::').underscore + end + + def reset + preferences.each do |name, _value| + set_preference name, preference_default(name) + end + end + + alias :[] :get_preference + alias :[]= :set_preference + + alias :get :get_preference + + def set(*args) + options = args.extract_options! + options.each do |name, value| + set_preference name, value + end + + return unless args.size == 2 + + set_preference args[0], args[1] + end + + def method_missing(method, *args) + name = method.to_s.gsub('=', '') + if has_preference? name + if method.to_s =~ /=$/ + set_preference(name, args.first) + else + get_preference name + end + else + super + end + end + end + end +end diff --git a/app/models/spree/preferences/preferable.rb b/app/models/spree/preferences/preferable.rb new file mode 100644 index 0000000000..76f4e6aad7 --- /dev/null +++ b/app/models/spree/preferences/preferable.rb @@ -0,0 +1,138 @@ +# frozen_string_literal: true + +# The preference_cache_key is used to determine if the preference +# can be set. The default behavior is to return nil if there is no +# id value. On ActiveRecords, new objects will have their preferences +# saved to a pending hash until it is persisted. +# +# class_attributes are inheritied unless you reassign them in +# the subclass, so when you inherit a Preferable class, the +# inherited hook will assign a new hash for the subclass definitions +# and copy all the definitions allowing the subclass to add +# additional defintions without affecting the base +module Spree + module Preferences + module Preferable + def self.included(base) + base.class_eval do + extend Spree::Preferences::PreferableClassMethods + + if respond_to?(:after_create) + after_create do |obj| + obj.save_pending_preferences + end + end + + if respond_to?(:after_destroy) + after_destroy do |obj| + obj.clear_preferences + end + end + end + end + + def get_preference(name) + has_preference! name + __send__ self.class.preference_getter_method(name) + end + alias :preferred :get_preference + alias :prefers? :get_preference + + def set_preference(name, value) + has_preference! name + __send__ self.class.preference_setter_method(name), value + end + + def preference_type(name) + has_preference! name + __send__ self.class.preference_type_getter_method(name) + end + + def preference_default(name) + has_preference! name + __send__ self.class.preference_default_getter_method(name) + end + + def preference_description(name) + has_preference! name + __send__ self.class.preference_description_getter_method(name) + end + + def has_preference!(name) + raise NoMethodError, "#{name} preference not defined" unless has_preference? name + end + + def has_preference?(name) + respond_to? self.class.preference_getter_method(name) + end + + def preferences + prefs = {} + methods.grep(/^prefers_.*\?$/).each do |pref_method| + prefs[pref_method.to_s.gsub(/prefers_|\?/, '').to_sym] = __send__(pref_method) + end + prefs + end + + def preference_cache_key(name) + return unless id + + [ENV["RAILS_CACHE_ID"], self.class.name, name, id].join('::').underscore + end + + def save_pending_preferences + return unless @pending_preferences + + @pending_preferences.each do |name, value| + set_preference(name, value) + end + end + + def clear_preferences + preferences.keys.each { |pref| preference_store.delete preference_cache_key(pref) } + end + + private + + def add_pending_preference(name, value) + @pending_preferences ||= {} + @pending_preferences[name] = value + end + + def get_pending_preference(name) + return unless @pending_preferences + + @pending_preferences[name] + end + + def convert_preference_value(value, type) + case type + when :string, :text + value.to_s + when :password + value.to_s + when :decimal + BigDecimal(value.to_s).round(2, BigDecimal::ROUND_HALF_UP) + when :integer + value.to_i + when :boolean + if value.is_a?(FalseClass) || + value.nil? || + value == 0 || + value =~ /^(f|false|0)$/i || + (value.respond_to?(:empty?) && value.empty?) + false + else + true + end + else + value + end + end + + def preference_store + Spree::Preferences::Store.instance + end + end + end +end diff --git a/app/models/spree/preferences/preferable_class_methods.rb b/app/models/spree/preferences/preferable_class_methods.rb new file mode 100644 index 0000000000..dd9369d3a0 --- /dev/null +++ b/app/models/spree/preferences/preferable_class_methods.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +module Spree + module Preferences + module PreferableClassMethods + def preference(name, type, *args) + options = args.extract_options! + options.assert_valid_keys(:default, :description) + default = options[:default] + description = options[:description] || name + + # cache_key will be nil for new objects, then if we check if there + # is a pending preference before going to default + define_method preference_getter_method(name) do + # perference_cache_key will only be nil/false for new records + # + if preference_cache_key(name) + preference_store.get(preference_cache_key(name), default) + else + get_pending_preference(name) || default + end + end + alias_method prefers_getter_method(name), preference_getter_method(name) + + define_method preference_setter_method(name) do |value| + value = convert_preference_value(value, type) + if preference_cache_key(name) + preference_store.set preference_cache_key(name), value, type + else + add_pending_preference(name, value) + end + end + alias_method prefers_setter_method(name), preference_setter_method(name) + + define_method preference_default_getter_method(name) do + default + end + + define_method preference_type_getter_method(name) do + type + end + + define_method preference_description_getter_method(name) do + description + end + end + + def remove_preference(name) + if method_defined? preference_getter_method(name) + remove_method preference_getter_method(name) + end + if method_defined? preference_setter_method(name) + remove_method preference_setter_method(name) + end + if method_defined? prefers_getter_method(name) + remove_method prefers_getter_method(name) + end + if method_defined? prefers_setter_method(name) + remove_method prefers_setter_method(name) + end + if method_defined? preference_default_getter_method(name) + remove_method preference_default_getter_method(name) + end + if method_defined? preference_type_getter_method(name) + remove_method preference_type_getter_method(name) + end + if method_defined? preference_description_getter_method(name) + remove_method preference_description_getter_method(name) + end + end + + def preference_getter_method(name) + "preferred_#{name}".to_sym + end + + def preference_setter_method(name) + "preferred_#{name}=".to_sym + end + + def prefers_getter_method(name) + "prefers_#{name}?".to_sym + end + + def prefers_setter_method(name) + "prefers_#{name}=".to_sym + end + + def preference_default_getter_method(name) + "preferred_#{name}_default".to_sym + end + + def preference_type_getter_method(name) + "preferred_#{name}_type".to_sym + end + + def preference_description_getter_method(name) + "preferred_#{name}_description".to_sym + end + end + end +end diff --git a/app/models/spree/preferences/store.rb b/app/models/spree/preferences/store.rb new file mode 100644 index 0000000000..cb30d31eaa --- /dev/null +++ b/app/models/spree/preferences/store.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +# Use singleton class Spree::Preferences::Store.instance to access +# +# StoreInstance has a persistence flag that is on by default, +# but we disable database persistence in testing to speed up tests +# + +require 'singleton' + +module Spree + module Preferences + class StoreInstance + attr_accessor :persistence + + def initialize + @cache = Rails.cache + @persistence = true + end + + def set(key, value, type) + @cache.write(key, value) + persist(key, value, type) + end + + def exist?(key) + @cache.exist?(key) || + should_persist? && Spree::Preference.where(key: key).exists? + end + + def get(key, fallback = nil) + # return the retrieved value, if it's in the cache + # use unless nil? incase the value is actually boolean false + # + unless (val = @cache.read(key)).nil? + return val + end + + if should_persist? + # If it's not in the cache, maybe it's in the database, but + # has been cleared from the cache + + # does it exist in the database? + if Spree::Preference.table_exists? && preference = Spree::Preference.find_by(key: key) + # it does exist, so let's put it back into the cache + @cache.write(preference.key, preference.value) + + # and return the value + return preference.value + end + end + + unless fallback.nil? + # cache fallback so we won't hit the db above on + # subsequent queries for the same key + # + @cache.write(key, fallback) + end + + fallback + end + + def delete(key) + @cache.delete(key) + destroy(key) + end + + def clear_cache + @cache.clear + end + + private + + def persist(cache_key, value, type) + return unless should_persist? + + preference = Spree::Preference.where(key: cache_key).first_or_initialize + preference.value = value + preference.value_type = type + preference.save + end + + def destroy(cache_key) + return unless should_persist? + + preference = Spree::Preference.find_by(key: cache_key) + preference&.destroy + end + + def should_persist? + @persistence && Spree::Preference.connected? && Spree::Preference.table_exists? + end + end + + class Store < StoreInstance + include Singleton + end + end +end diff --git a/app/views/spree/admin/orders/customer_details/_address_form.html.haml b/app/views/spree/admin/orders/customer_details/_address_form.html.haml index 18bd0970b6..da8d943c18 100644 --- a/app/views/spree/admin/orders/customer_details/_address_form.html.haml +++ b/app/views/spree/admin/orders/customer_details/_address_form.html.haml @@ -16,10 +16,6 @@ %div{class: "field"} = f.label :lastname, Spree.t(:last_name) + ':' = f.text_field :lastname, class: 'fullwidth' - - if Spree::Config[:company] - %div{class: "field"} - = f.label :company, Spree.t(:company) + ':' - = f.text_field :company, class: 'fullwidth' %div{class: "field"} = f.label :address1, Spree.t(:street_address) + ':' = f.text_field :address1, class: 'fullwidth' diff --git a/lib/spree/core/controller_helpers/common.rb b/lib/spree/core/controller_helpers/common.rb index e88920eaf6..c1f12fdfc0 100644 --- a/lib/spree/core/controller_helpers/common.rb +++ b/lib/spree/core/controller_helpers/common.rb @@ -37,11 +37,7 @@ module Spree def title title_string = @title.presence || accurate_title if title_string.present? - if Spree::Config[:always_put_site_name_in_title] - [title_string, default_title].join(' - ') - else - title_string - end + [title_string, default_title].join(' - ') else default_title end diff --git a/spec/features/admin/configuration/general_settings_spec.rb b/spec/features/admin/configuration/general_settings_spec.rb index e676f32801..7a9299a634 100644 --- a/spec/features/admin/configuration/general_settings_spec.rb +++ b/spec/features/admin/configuration/general_settings_spec.rb @@ -12,19 +12,19 @@ describe "General Settings" do context "visiting general settings (admin)" do it "should have the right content" do expect(page).to have_content("General Settings") - expect(find("#site_name").value).to eq("Spree Demo Site") - expect(find("#site_url").value).to eq("demo.spreecommerce.com") + expect(find("#site_name").value).to eq("OFN Demo Site") + expect(find("#site_url").value).to eq("demo.openfoodnetwork.org") end end context "editing general settings (admin)" do it "should be able to update the site name" do - fill_in "site_name", with: "Spree Demo Site99" + fill_in "site_name", with: "OFN Demo Site99" click_button "Update" assert_successful_update_message(:general_settings) - expect(find("#site_name").value).to eq("Spree Demo Site99") + expect(find("#site_name").value).to eq("OFN Demo Site99") end def assert_successful_update_message(resource) diff --git a/spec/models/spree/app_configuration_spec.rb b/spec/models/spree/app_configuration_spec.rb new file mode 100644 index 0000000000..607dc8501b --- /dev/null +++ b/spec/models/spree/app_configuration_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Spree::AppConfiguration do + let(:prefs) { Rails.application.config.spree.preferences } + + it "should be available from the environment" do + prefs.site_name = "TEST SITE NAME" + expect(prefs.site_name).to eq "TEST SITE NAME" + end + + it "should be available as Spree::Config for legacy access" do + Spree::Config.site_name = "Spree::Config TEST SITE NAME" + expect(Spree::Config.site_name).to eq "Spree::Config TEST SITE NAME" + end +end diff --git a/spec/models/spree/preference_spec.rb b/spec/models/spree/preference_spec.rb new file mode 100644 index 0000000000..780434f768 --- /dev/null +++ b/spec/models/spree/preference_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Spree::Preference do + it "should require a key" do + @preference = Spree::Preference.new + @preference.key = :test + @preference.value_type = :boolean + @preference.value = true + expect(@preference).to be_valid + end + + describe "type coversion for values" do + def round_trip_preference(key, value, value_type) + p = Spree::Preference.new + p.value = value + p.value_type = value_type + p.key = key + p.save + + Spree::Preference.find_by(key: key) + end + + it ":boolean" do + value_type = :boolean + value = true + key = "boolean_key" + pref = round_trip_preference(key, value, value_type) + expect(pref.value).to eq value + expect(pref.value_type).to eq value_type.to_s + end + + it "false :boolean" do + value_type = :boolean + value = false + key = "boolean_key" + pref = round_trip_preference(key, value, value_type) + expect(pref.value).to eq value + expect(pref.value_type).to eq value_type.to_s + end + + it ":integer" do + value_type = :integer + value = 10 + key = "integer_key" + pref = round_trip_preference(key, value, value_type) + expect(pref.value).to eq value + expect(pref.value_type).to eq value_type.to_s + end + + it ":decimal" do + value_type = :decimal + value = 1.5 + key = "decimal_key" + pref = round_trip_preference(key, value, value_type) + expect(pref.value).to eq value + expect(pref.value_type).to eq value_type.to_s + end + + it ":string" do + value_type = :string + value = "This is a string" + key = "string_key" + pref = round_trip_preference(key, value, value_type) + expect(pref.value).to eq value + expect(pref.value_type).to eq value_type.to_s + end + + it ":text" do + value_type = :text + value = "This is a string stored as text" + key = "text_key" + pref = round_trip_preference(key, value, value_type) + expect(pref.value).to eq value + expect(pref.value_type).to eq value_type.to_s + end + + it ":password" do + value_type = :password + value = "This is a password" + key = "password_key" + pref = round_trip_preference(key, value, value_type) + expect(pref.value).to eq value + expect(pref.value_type).to eq value_type.to_s + end + + it ":any" do + value_type = :any + value = [1, 2] + key = "any_key" + pref = round_trip_preference(key, value, value_type) + expect(pref.value).to eq value + expect(pref.value_type).to eq value_type.to_s + end + end +end diff --git a/spec/models/spree/preferences/configuration_spec.rb b/spec/models/spree/preferences/configuration_spec.rb new file mode 100644 index 0000000000..32883d3fb6 --- /dev/null +++ b/spec/models/spree/preferences/configuration_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Spree::Preferences::Configuration do + before :all do + class AppConfig < Spree::Preferences::Configuration + preference :color, :string, default: :blue + end + @config = AppConfig.new + end + + it "has named methods to access preferences" do + @config.color = 'orange' + expect(@config.color).to eq 'orange' + end + + it "uses [ ] to access preferences" do + @config[:color] = 'red' + expect(@config[:color]).to eq 'red' + end + + it "uses set/get to access preferences" do + @config.set :color, 'green' + expect(@config.get(:color)).to eq 'green' + end +end diff --git a/spec/models/spree/preferences/preferable_spec.rb b/spec/models/spree/preferences/preferable_spec.rb new file mode 100644 index 0000000000..3e156fce1f --- /dev/null +++ b/spec/models/spree/preferences/preferable_spec.rb @@ -0,0 +1,326 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Spree::Preferences::Preferable do + before :all do + class A + include Spree::Preferences::Preferable + attr_reader :id + + def initialize + @id = rand(999) + end + + preference :color, :string, default: 'green', description: "My Favorite Color" + end + + class B < A + preference :flavor, :string + end + end + + before :each do + @a = A.new + allow(@a).to receive_messages(persisted?: true) + @b = B.new + allow(@b).to receive_messages(persisted?: true) + + # ensure we're persisting as that is the default + # + store = Spree::Preferences::Store.instance + store.persistence = true + end + + describe "preference definitions" do + it "parent should not see child definitions" do + expect(@a.has_preference?(:color)).to be_truthy + expect(@a.has_preference?(:flavor)).not_to be_truthy + end + + it "child should have parent and own definitions" do + expect(@b.has_preference?(:color)).to be_truthy + expect(@b.has_preference?(:flavor)).to be_truthy + end + + it "instances have defaults" do + expect(@a.preferred_color).to eq 'green' + expect(@b.preferred_color).to eq 'green' + expect(@b.preferred_flavor).to be_nil + end + + it "can be asked if it has a preference definition" do + expect(@a.has_preference?(:color)).to be_truthy + expect(@a.has_preference?(:bad)).to be_falsy + end + + it "can be asked and raises" do + expect { + @a.has_preference! :flavor + }.to raise_error(NoMethodError, "flavor preference not defined") + end + + it "has a type" do + expect(@a.preferred_color_type).to eq :string + expect(@a.preference_type(:color)).to eq :string + end + + it "has a default" do + expect(@a.preferred_color_default).to eq 'green' + expect(@a.preference_default(:color)).to eq 'green' + end + + it "has a description" do + expect(@a.preferred_color_description).to eq "My Favorite Color" + expect(@a.preference_description(:color)).to eq "My Favorite Color" + end + + it "raises if not defined" do + expect { + @a.get_preference :flavor + }.to raise_error(NoMethodError, "flavor preference not defined") + end + end + + describe "preference access" do + it "handles ghost methods for preferences" do + @a.preferred_color = 'blue' + expect(@a.preferred_color).to eq 'blue' + + @a.prefers_color = 'green' + expect(@a.prefers_color?).to eq 'green' + end + + it "has genric readers" do + @a.preferred_color = 'red' + expect(@a.prefers?(:color)).to eq 'red' + expect(@a.preferred(:color)).to eq 'red' + end + + it "parent and child instances have their own prefs" do + @a.preferred_color = 'red' + @b.preferred_color = 'blue' + + expect(@a.preferred_color).to eq 'red' + expect(@b.preferred_color).to eq 'blue' + end + + it "raises when preference not defined" do + expect { + @a.set_preference(:bad, :bone) + }.to raise_exception(NoMethodError, "bad preference not defined") + end + + it "builds a hash of preferences" do + @b.preferred_flavor = :strawberry + expect(@b.preferences[:flavor]).to eq 'strawberry' + expect(@b.preferences[:color]).to eq 'green' # default from A + end + + context "database fallback" do + before do + @a.instance_variable_set("@pending_preferences", {}) + end + + it "retrieves a preference from the database before falling back to default" do + preference = double(value: "chatreuse", key: 'a/color/123') + expect(Spree::Preference).to receive(:find_by).and_return(preference) + expect(@a.preferred_color).to eq 'chatreuse' + end + + it "defaults if no database key exists" do + expect(Spree::Preference).to receive(:find_by).and_return(nil) + expect(@a.preferred_color).to eq 'green' + end + end + + context "converts integer preferences to integer values" do + before do + A.preference :is_integer, :integer + end + + it "with strings" do + @a.set_preference(:is_integer, '3') + expect(@a.preferences[:is_integer]).to eq 3 + + @a.set_preference(:is_integer, '') + expect(@a.preferences[:is_integer]).to eq 0 + end + end + + context "converts decimal preferences to BigDecimal values" do + before do + A.preference :if_decimal, :decimal + end + + it "returns a BigDecimal" do + @a.set_preference(:if_decimal, 3.3) + expect(@a.preferences[:if_decimal].class).to eq BigDecimal + end + + it "with strings" do + @a.set_preference(:if_decimal, '3.3') + expect(@a.preferences[:if_decimal]).to eq 3.3 + + @a.set_preference(:if_decimal, '') + expect(@a.preferences[:if_decimal]).to eq 0.0 + end + end + + context "converts boolean preferences to boolean values" do + before do + A.preference :is_boolean, :boolean, default: true + end + + it "with strings" do + @a.set_preference(:is_boolean, '0') + expect(@a.preferences[:is_boolean]).to be_falsy + @a.set_preference(:is_boolean, 'f') + expect(@a.preferences[:is_boolean]).to be_falsy + @a.set_preference(:is_boolean, 't') + expect(@a.preferences[:is_boolean]).to be_truthy + end + + it "with integers" do + @a.set_preference(:is_boolean, 0) + expect(@a.preferences[:is_boolean]).to be_falsy + @a.set_preference(:is_boolean, 1) + expect(@a.preferences[:is_boolean]).to be_truthy + end + + it "with an empty string" do + @a.set_preference(:is_boolean, '') + expect(@a.preferences[:is_boolean]).to be_falsy + end + + it "with an empty hash" do + @a.set_preference(:is_boolean, []) + expect(@a.preferences[:is_boolean]).to be_falsy + end + end + + context "converts any preferences to any values" do + before do + A.preference :product_ids, :any, default: [] + A.preference :product_attributes, :any, default: {} + end + + it "with array" do + expect(@a.preferences[:product_ids]).to eq [] + @a.set_preference(:product_ids, [1, 2]) + expect(@a.preferences[:product_ids]).to eq [1, 2] + end + + it "with hash" do + expect(@a.preferences[:product_attributes]).to eq({}) + @a.set_preference(:product_attributes, { id: 1, name: 2 }) + attributes_hash = { id: 1, name: 2 } + expect(@a.preferences[:product_attributes]).to eq attributes_hash + end + end + end + + describe "persisted preferables" do + before(:all) do + class CreatePrefTest < ActiveRecord::Migration + def self.up + create_table :pref_tests do |t| + t.string :col + end + end + + def self.down + drop_table :pref_tests + end + end + + @migration_verbosity = ActiveRecord::Migration.verbose + ActiveRecord::Migration.verbose = false + CreatePrefTest.migrate(:up) + + class PrefTest < ActiveRecord::Base + preference :pref_test_pref, :string, default: 'abc' + preference :pref_test_any, :any, default: [] + end + end + + after(:all) do + CreatePrefTest.migrate(:down) + ActiveRecord::Migration.verbose = @migration_verbosity + end + + before(:each) do + @pt = PrefTest.create + end + + describe "pending preferences for new activerecord objects" do + it "saves preferences after record is saved" do + pr = PrefTest.new + pr.set_preference(:pref_test_pref, 'XXX') + expect(pr.get_preference(:pref_test_pref)).to eq 'XXX' + pr.save! + expect(pr.get_preference(:pref_test_pref)).to eq 'XXX' + end + + it "saves preferences for serialized object" do + pr = PrefTest.new + pr.set_preference(:pref_test_any, [1, 2]) + expect(pr.get_preference(:pref_test_any)).to eq [1, 2] + pr.save! + expect(pr.get_preference(:pref_test_any)).to eq [1, 2] + end + end + + describe "requires a valid id" do + it "for cache_key" do + pref_test = PrefTest.new + expect(pref_test.preference_cache_key(:pref_test_pref)).to be_nil + + pref_test.save + expect(pref_test.preference_cache_key(:pref_test_pref)).to_not be_nil + end + + it "but returns default values" do + pref_test = PrefTest.new + expect(pref_test.get_preference(:pref_test_pref)).to eq 'abc' + end + + it "adds prefs in a pending hash until after_create" do + pref_test = PrefTest.new + expect(pref_test).to receive(:add_pending_preference).with(:pref_test_pref, 'XXX') + pref_test.set_preference(:pref_test_pref, 'XXX') + end + end + + it "clear preferences" do + @pt.set_preference(:pref_test_pref, 'xyz') + expect(@pt.preferred_pref_test_pref).to eq 'xyz' + @pt.clear_preferences + expect(@pt.preferred_pref_test_pref).to eq 'abc' + end + + it "clear preferences when record is deleted" do + @pt.save! + @pt.preferred_pref_test_pref = 'lmn' + @pt.save! + @pt.destroy + @pt1 = PrefTest.new(col: 'aaaa') + @pt1.id = @pt.id + @pt1.save! + expect(@pt1.get_preference(:pref_test_pref)).to_not eq 'lmn' + expect(@pt1.get_preference(:pref_test_pref)).to eq 'abc' + end + end + + it "builds cache keys" do + expect(@a.preference_cache_key(:color)).to match %r{a/color/\d+} + end + + it "can add and remove preferences" do + A.preference :test_temp, :boolean, default: true + expect(@a.preferred_test_temp).to be_truthy + A.remove_preference :test_temp + expect(@a.has_preference?(:test_temp)).to be_falsy + expect(@a.respond_to?(:preferred_test_temp)).to be_falsy + end +end diff --git a/spec/models/spree/preferences/store_spec.rb b/spec/models/spree/preferences/store_spec.rb new file mode 100644 index 0000000000..7d5995e97a --- /dev/null +++ b/spec/models/spree/preferences/store_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Spree::Preferences::Store do + before :each do + @store = Spree::Preferences::StoreInstance.new + end + + it "sets and gets a key" do + @store.set :test, 1, :integer + expect(@store.exist?(:test)).to be_truthy + expect(@store.get(:test)).to eq 1 + end + + it "can set and get false values when cache return nil" do + @store.set :test, false, :boolean + expect(@store.get(:test)).to be_falsy + end + + it "will return db value when cache is emtpy and cache the db value" do + preference = Spree::Preference.where(key: 'test').first_or_initialize + preference.value = '123' + preference.value_type = 'string' + preference.save + + Rails.cache.clear + expect(@store.get(:test)).to eq '123' + expect(Rails.cache.read(:test)).to eq '123' + end + + it "should return and cache fallback value when supplied" do + Rails.cache.clear + expect(@store.get(:test, false)).to be_falsy + expect(Rails.cache.read(:test)).to be_falsy + end + + it "should return and cache fallback value when persistence is disabled (i.e. on bootstrap)" do + Rails.cache.clear + allow(@store).to receive_messages(should_persist?: false) + expect(@store.get(:test, true)).to be_truthy + expect(Rails.cache.read(:test)).to be_truthy + end + + it "should return nil when key can't be found and fallback value is not supplied" do + expect(@store.get(:random_key)).to be_nil + end +end