diff --git a/.gitignore b/.gitignore index c3f71e6055..a8d86c558c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,6 @@ log .sass-cache spec/dummy/app/assets/builds/* !spec/dummy/app/assets/builds/.keep -spec/dummy/config/alchemy/config.yml spec/dummy/db/*.sqlite3* spec/dummy/postcss.config.js spec/dummy/public/assets/ diff --git a/README.md b/README.md index d2692751fb..787d160d5b 100644 --- a/README.md +++ b/README.md @@ -246,7 +246,7 @@ $ bin/rake alchemy:upgrade Alchemy will print out useful information after running the automated tasks that help a smooth upgrade path. So please **take your time and read them**. -Always be sure to keep an eye on the `config/alchemy/config.yml.defaults` file and update your `config/alchemy/config.yml` accordingly. +Always be sure to keep an eye on the output of your Rails app when starting. There will probably be useful information about deprecations. Also, `git diff` is your friend. diff --git a/app/controllers/alchemy/admin/languages_controller.rb b/app/controllers/alchemy/admin/languages_controller.rb index ee487b3129..e33059cdb1 100644 --- a/app/controllers/alchemy/admin/languages_controller.rb +++ b/app/controllers/alchemy/admin/languages_controller.rb @@ -14,7 +14,7 @@ def index def new @language = Language.new( site: @current_site, - page_layout: Config.get(:default_language)["page_layout"] + page_layout: Alchemy.config.get(:default_language)["page_layout"] ) end diff --git a/app/controllers/alchemy/admin/pages_controller.rb b/app/controllers/alchemy/admin/pages_controller.rb index 986878eecd..1749cf4a57 100644 --- a/app/controllers/alchemy/admin/pages_controller.rb +++ b/app/controllers/alchemy/admin/pages_controller.rb @@ -69,7 +69,7 @@ def show Current.preview_page = @page # Setting the locale to pages language, so the page content has it's correct translations. ::I18n.locale = @page.language.locale - render(layout: Alchemy::Config.get(:admin_page_preview_layout) || "application") + render(layout: Alchemy.config.get(:admin_page_preview_layout) || "application") end def info diff --git a/app/controllers/alchemy/admin/resources_controller.rb b/app/controllers/alchemy/admin/resources_controller.rb index ed5749dedb..9e9a1e7ab6 100644 --- a/app/controllers/alchemy/admin/resources_controller.rb +++ b/app/controllers/alchemy/admin/resources_controller.rb @@ -224,11 +224,11 @@ def permitted_ransack_search_fields def items_per_page cookies[:alchemy_items_per_page] = - params[:per_page] || cookies[:alchemy_items_per_page] || Alchemy::Config.get(:items_per_page) + params[:per_page] || cookies[:alchemy_items_per_page] || Alchemy.config.get(:items_per_page) end def items_per_page_options - per_page = Alchemy::Config.get(:items_per_page) + per_page = Alchemy.config.get(:items_per_page) [per_page, per_page * 2, per_page * 4] end diff --git a/app/controllers/alchemy/messages_controller.rb b/app/controllers/alchemy/messages_controller.rb index b84ac401d2..388ebe4304 100644 --- a/app/controllers/alchemy/messages_controller.rb +++ b/app/controllers/alchemy/messages_controller.rb @@ -74,7 +74,7 @@ def create # :nodoc: private def mailer_config - Alchemy::Config.get(:mailer) + Alchemy.config.get(:mailer) end def mail_to diff --git a/app/helpers/alchemy/admin/base_helper.rb b/app/helpers/alchemy/admin/base_helper.rb index 9326f052a1..e9d0f8839e 100644 --- a/app/helpers/alchemy/admin/base_helper.rb +++ b/app/helpers/alchemy/admin/base_helper.rb @@ -273,7 +273,7 @@ def clipboard_select_tag_options(items) # Returns the regular expression used for external url validation in link dialog. def link_url_regexp - Alchemy::Config.get(:format_matchers)["link_url"] || /^(mailto:|\/|[a-z]+:\/\/)/ + Alchemy.config.get(:format_matchers)["link_url"] || /^(mailto:|\/|[a-z]+:\/\/)/ end # Renders a hint with tooltip diff --git a/app/helpers/alchemy/admin/pages_helper.rb b/app/helpers/alchemy/admin/pages_helper.rb index fe502e91c3..957c247221 100644 --- a/app/helpers/alchemy/admin/pages_helper.rb +++ b/app/helpers/alchemy/admin/pages_helper.rb @@ -10,7 +10,7 @@ module PagesHelper # You can configure the screen sizes in your +config/alchemy/config.yml+. # def preview_sizes_for_select - Alchemy::Config.get(:page_preview_sizes).map do |size| + Alchemy.config.get(:page_preview_sizes).map do |size| [Alchemy.t(size, scope: "preview_sizes"), size] end end diff --git a/app/models/alchemy/attachment.rb b/app/models/alchemy/attachment.rb index d0d76c9edd..251edb5023 100644 --- a/app/models/alchemy/attachment.rb +++ b/app/models/alchemy/attachment.rb @@ -83,12 +83,12 @@ def searchable_alchemy_resource_attributes end def allowed_filetypes - Config.get(:uploader).fetch("allowed_filetypes", {}).fetch("alchemy/attachments", []) + Alchemy.config.get(:uploader).fetch("allowed_filetypes", {}).fetch("alchemy/attachments", []) end end validates_presence_of :file - validates_size_of :file, maximum: Config.get(:uploader)["file_size_limit"].megabytes + validates_size_of :file, maximum: Alchemy.config.get(:uploader)["file_size_limit"].megabytes validates_property :ext, of: :file, in: allowed_filetypes, diff --git a/app/models/alchemy/base_record.rb b/app/models/alchemy/base_record.rb index 58e7044d9e..c005a18344 100644 --- a/app/models/alchemy/base_record.rb +++ b/app/models/alchemy/base_record.rb @@ -6,6 +6,8 @@ def self.table_name_prefix end class BaseRecord < ActiveRecord::Base + include ConfigMissing + extend ConfigMissing extend Alchemy::SearchableResource self.abstract_class = true diff --git a/app/models/alchemy/ingredient_validator.rb b/app/models/alchemy/ingredient_validator.rb index a49015e97d..53ef6b07a4 100644 --- a/app/models/alchemy/ingredient_validator.rb +++ b/app/models/alchemy/ingredient_validator.rb @@ -80,8 +80,12 @@ def validate_uniqueness(*) end def validate_format(format) - matcher = Alchemy::Config.get("format_matchers")[format] || format - if !ingredient.value.to_s.match?(Regexp.new(matcher)) + matcher = if format.is_a?(String) || format.is_a?(Symbol) + Alchemy.config.get("format_matchers").get(format) + else + format + end + if !ingredient.value.to_s.match?(matcher) ingredient.errors.add(:value, :invalid) end end diff --git a/app/models/alchemy/message.rb b/app/models/alchemy/message.rb index 64326e07c5..3ea9a42687 100644 --- a/app/models/alchemy/message.rb +++ b/app/models/alchemy/message.rb @@ -17,7 +17,7 @@ class Message include ActiveModel::Model def self.config - Alchemy::Config.get(:mailer) + Alchemy.config.get(:mailer) end attr_accessor :contact_form_id, :ip @@ -32,7 +32,7 @@ def self.config case field.to_sym when /email/ validates_format_of field, - with: Alchemy::Config.get("format_matchers")["email"], + with: Alchemy.config.get("format_matchers")["email"], if: -> { send(field).present? } when :email_confirmation validates_confirmation_of :email diff --git a/app/models/alchemy/page.rb b/app/models/alchemy/page.rb index 8d4666d40a..97648054f9 100644 --- a/app/models/alchemy/page.rb +++ b/app/models/alchemy/page.rb @@ -257,7 +257,7 @@ def all_from_clipboard_for_select(clipboard, language_id, layoutpages: false) def link_target_options options = [[Alchemy.t(:default, scope: "link_target_options"), ""]] - link_target_options = Config.get(:link_target_options) + link_target_options = Alchemy.config.get(:link_target_options) link_target_options.each do |option| # add an underscore to the options to provide the default syntax # @link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#target diff --git a/app/models/alchemy/page/page_natures.rb b/app/models/alchemy/page/page_natures.rb index e42cdc5c3e..413500ed07 100644 --- a/app/models/alchemy/page/page_natures.rb +++ b/app/models/alchemy/page/page_natures.rb @@ -153,7 +153,7 @@ def cache_page? private def caching_enabled? - Alchemy::Config.get(:cache_pages) && + Alchemy.config.get(:cache_pages) && Rails.application.config.action_controller.perform_caching end end diff --git a/app/models/alchemy/picture.rb b/app/models/alchemy/picture.rb index 11b6185904..84b0013869 100644 --- a/app/models/alchemy/picture.rb +++ b/app/models/alchemy/picture.rb @@ -100,12 +100,12 @@ def self.preprocessor_class=(klass) # We need to define this method here to have it available in the validations below. class << self def allowed_filetypes - Config.get(:uploader).fetch("allowed_filetypes", {}).fetch("alchemy/pictures", []) + Alchemy.config.get(:uploader).fetch("allowed_filetypes", {}).fetch("alchemy/pictures", []) end end validates_presence_of :image_file - validates_size_of :image_file, maximum: Config.get(:uploader)["file_size_limit"].megabytes + validates_size_of :image_file, maximum: Alchemy.config.get(:uploader)["file_size_limit"].megabytes validates_property :format, of: :image_file, in: allowed_filetypes, @@ -260,7 +260,7 @@ def humanized_name # def default_render_format if convertible? - Config.get(:image_output_format) + Alchemy.config.get(:image_output_format) else image_file_format end @@ -272,8 +272,8 @@ def default_render_format # image has not a convertible file format (i.e. SVG) this returns +false+ # def convertible? - Config.get(:image_output_format) && - Config.get(:image_output_format) != "original" && + Alchemy.config.get(:image_output_format) && + Alchemy.config.get(:image_output_format) != "original" && has_convertible_format? end diff --git a/app/models/alchemy/picture/preprocessor.rb b/app/models/alchemy/picture/preprocessor.rb index 86d2869602..7584256788 100644 --- a/app/models/alchemy/picture/preprocessor.rb +++ b/app/models/alchemy/picture/preprocessor.rb @@ -9,12 +9,12 @@ def initialize(image_file) # Preprocess images after upload # - # Define preprocessing options in the Alchemy::Config + # Define preprocessing options in the Alchemy.config # # preprocess_image_resize [String] - Downsizing example: '1000x1000>' # def call - max_image_size = Alchemy::Config.get(:preprocess_image_resize) + max_image_size = Alchemy.config.get(:preprocess_image_resize) image_file.thumb!(max_image_size) if max_image_size.present? # Auto orient the image so EXIF orientation data is taken into account image_file.auto_orient! diff --git a/app/models/alchemy/picture_variant.rb b/app/models/alchemy/picture_variant.rb index 8e2f291d75..4d074f285b 100644 --- a/app/models/alchemy/picture_variant.rb +++ b/app/models/alchemy/picture_variant.rb @@ -97,7 +97,7 @@ def encoded_image(image, options = {}) convert_format = render_format.sub("jpeg", "jpg") != picture.image_file_format.sub("jpeg", "jpg") if encodable_image? && (convert_format || options[:quality]) - quality = options[:quality] || Config.get(:output_image_quality) + quality = options[:quality] || Alchemy.config.get(:output_image_quality) encoding_options << "-quality #{quality}" end diff --git a/app/models/concerns/alchemy/picture_thumbnails.rb b/app/models/concerns/alchemy/picture_thumbnails.rb index aa4e907233..4c6d6485d5 100644 --- a/app/models/concerns/alchemy/picture_thumbnails.rb +++ b/app/models/concerns/alchemy/picture_thumbnails.rb @@ -28,7 +28,7 @@ module PictureThumbnails # # @option options format [String] # The format the picture should be rendered in. - # Defaults to the +image_output_format+ from the +Alchemy::Config+. + # Defaults to the +image_output_format+ from the +Alchemy.config+. # # @option options crop [Boolean] # If set to true the picture will be cropped to fit the size value. diff --git a/app/views/alchemy/admin/languages/index.html.erb b/app/views/alchemy/admin/languages/index.html.erb index 9d2f68aa38..e412554bde 100644 --- a/app/views/alchemy/admin/languages/index.html.erb +++ b/app/views/alchemy/admin/languages/index.html.erb @@ -31,12 +31,15 @@ <%== Alchemy.t('alchemy/language', scope: :no_resource_found) %> <% end %> <%= render 'form', language: Alchemy::Language.new( - Alchemy::Config.get(:default_language).merge( + name: Alchemy.config.default_language.name, + code: Alchemy.config.default_language.code, + page_layout: Alchemy.config.default_language.page_layout, + frontpage_name: Alchemy.config.default_language.frontpage_name, site: Alchemy::Current.site, default: true, public: true ) - ) %> + %> <% end %> diff --git a/app/views/alchemy/admin/sites/index.html.erb b/app/views/alchemy/admin/sites/index.html.erb index b084ddd532..ef2c7004e4 100644 --- a/app/views/alchemy/admin/sites/index.html.erb +++ b/app/views/alchemy/admin/sites/index.html.erb @@ -30,7 +30,11 @@ <%= render_message do %> <%== Alchemy.t('alchemy/site', scope: :no_resource_found) %> <% end %> - <%= render 'form', site: Alchemy::Site.new(Alchemy::Config.get(:default_site).merge(public: true)) %> + <%= render 'form', site: Alchemy::Site.new( + name: Alchemy.config.default_site.name, + host: Alchemy.config.default_site.host, + public: true + ) %> <% end %> diff --git a/lib/alchemy.rb b/lib/alchemy.rb index a3703e1a32..4bf0cd7817 100644 --- a/lib/alchemy.rb +++ b/lib/alchemy.rb @@ -2,10 +2,23 @@ require "alchemy/admin/preview_url" require "importmap-rails" +require "alchemy/configurations/main" +require "alchemy/config_missing" module Alchemy + include Alchemy::ConfigMissing + extend Alchemy::ConfigMissing + YAML_PERMITTED_CLASSES = %w[Symbol Date Regexp] + def self.config + @_config ||= Alchemy::Configurations::Main.new + end + + def self.configure(&blk) + yield config + end + # Define page preview sources # # A preview source is a Ruby class returning an URL diff --git a/lib/alchemy/admin/preview_url.rb b/lib/alchemy/admin/preview_url.rb index dba6d96ff1..9408c579e8 100644 --- a/lib/alchemy/admin/preview_url.rb +++ b/lib/alchemy/admin/preview_url.rb @@ -41,7 +41,6 @@ def initialize(routes:) def url_for(page) @preview_config = preview_config_for(page) - if @preview_config && uri uri_class.build( host: uri.host, @@ -60,10 +59,10 @@ def url_for(page) attr_reader :routes def preview_config_for(page) - preview_config = Alchemy::Config.get(:preview) + preview_config = Alchemy.config.preview return unless preview_config - preview_config[page.site.name] || preview_config + preview_config.for_site(page.site) || preview_config end def uri @@ -81,8 +80,8 @@ def uri_class end def userinfo - auth = @preview_config["auth"] - auth ? "#{auth["username"]}:#{auth["password"]}" : nil + auth = @preview_config.auth + auth.username ? "#{auth["username"]}:#{auth["password"]}" : nil end end end diff --git a/lib/alchemy/config.rb b/lib/alchemy/config.rb deleted file mode 100644 index 127edb0e42..0000000000 --- a/lib/alchemy/config.rb +++ /dev/null @@ -1,114 +0,0 @@ -# frozen_string_literal: true - -module Alchemy - class Config - class << self - # Returns the configuration for given parameter name. - # - # @param name [String] - # - def get(name) - check_deprecation(name) - key = check_replacement(name) - show[key.to_s] - end - - alias_method :parameter, :get - - # Returns a merged configuration of the following files - # - # Alchemys default config: +gems/../alchemy_cms/config/alchemy/config.yml+ - # Your apps default config: +your_app/config/alchemy/config.yml+ - # Environment specific config: +your_app/config/alchemy/development.config.yml+ - # - # An environment specific config overwrites the settings of your apps default config, - # while your apps default config has precedence over Alchemys default config. - # - def show - @config ||= merge_configs!(alchemy_config, main_app_config, env_specific_config) - end - - # A list of deprecated configuration values - # a value of nil means there is no new default - # any not nil value is the new default - def deprecated_configs - {} - end - - # A list of replaced configuration keys - def replaced_config_keys - { - output_image_quality: :output_image_jpg_quality - } - end - - private - - # Alchemy default configuration - def alchemy_config - read_file Engine.root.join("config/alchemy/config.yml") - end - - # Application specific configuration - def main_app_config - read_file Rails.root.join("config/alchemy/config.yml") - end - - # Rails Environment specific configuration - def env_specific_config - read_file Rails.root.join("config/alchemy/#{Rails.env}.config.yml") - end - - # Tries to load yaml file from given path. - # If it does not exist, or its empty, it returns an empty Hash. - # - def read_file(file) - YAML.safe_load( - ERB.new(File.read(file)).result, - permitted_classes: YAML_PERMITTED_CLASSES, - aliases: true - ) || {} - rescue Errno::ENOENT - {} - end - - # Merges all given configs together - # - def merge_configs!(*config_files) - raise LoadError, "No Alchemy config file found!" if config_files.map(&:blank?).all? - - config = {} - config_files.each { |h| config.merge!(h.stringify_keys!) } - config - end - - def check_deprecation(name) - if deprecated_configs.key?(name.to_sym) - config = deprecated_configs[name.to_sym] - if config.nil? - Alchemy::Deprecation.warn("#{name} configuration is deprecated and will be removed from Alchemy #{Alchemy::Deprecation.deprecation_horizon}") - else - value = show[name.to_s] - if value != config - Alchemy::Deprecation.warn("Setting #{name} configuration to #{value} is deprecated and will be always #{config} in Alchemy #{Alchemy::Deprecation.deprecation_horizon}") - end - end - end - end - - def check_replacement(name) - if replaced_config_keys.key?(name.to_sym) - old_key = replaced_config_keys[name.to_sym] - if show[old_key.to_s] - Alchemy::Deprecation.warn("Using #{old_key} configuration is deprecated and will be removed in Alchemy #{Alchemy::Deprecation.deprecation_horizon}. Please use #{name} instead.") - old_key - else - name - end - else - name - end - end - end - end -end diff --git a/lib/alchemy/config_missing.rb b/lib/alchemy/config_missing.rb new file mode 100644 index 0000000000..2aed42bc87 --- /dev/null +++ b/lib/alchemy/config_missing.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Alchemy + module ConfigMissing + def const_missing(missing_const_name) + if missing_const_name == :Config + Alchemy::Deprecation.warn("Alchemy::Config is deprecated. Use Alchemy.config instead.") + Alchemy.config + else + super + end + end + end +end diff --git a/lib/alchemy/configuration.rb b/lib/alchemy/configuration.rb new file mode 100644 index 0000000000..3c0503ca75 --- /dev/null +++ b/lib/alchemy/configuration.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +require "active_support" +require "active_support/core_ext/string" + +require "alchemy/configuration/boolean_option" +require "alchemy/configuration/class_option" +require "alchemy/configuration/class_set_option" +require "alchemy/configuration/integer_option" +require "alchemy/configuration/integer_list_option" +require "alchemy/configuration/regexp_option" +require "alchemy/configuration/string_list_option" +require "alchemy/configuration/string_option" + +module Alchemy + class Configuration + def initialize(configuration_hash = {}) + set(configuration_hash) + end + + def set(configuration_hash) + configuration_hash.each do |key, value| + send(:"#{key}=", value) + end + end + + alias_method :get, :send + alias_method :[], :get + + def show = self + + def fetch(key, default = nil) + get(key) || default + end + + def set_from_yaml(file) + set( + YAML.safe_load( + ERB.new(File.read(file)).result, + permitted_classes: YAML_PERMITTED_CLASSES, + aliases: true + ) || {} + ) + end + + def to_h + self.class.defined_options.map do |option| + [option, send(option)] + end.concat( + self.class.defined_configurations.map do |configuration| + [configuration, send(configuration).to_h] + end + ).to_h + end + + class << self + def defined_configurations = [] + + def defined_options = [] + + def configuration(name, configuration_class) + # The defined configurations on a class are all those defined directly on + # that class as well as those defined on ancestors. + # We store these as a class instance variable on each class which has a + # configuration. super() collects configurations defined on ancestors. + singleton_configurations = (@defined_singleton_configurations ||= []) + singleton_configurations << name.to_sym + + define_singleton_method :defined_configurations do + super() + singleton_configurations + end + + define_method(name) do + unless instance_variable_get(:"@#{name}") + send(:"#{name}=", configuration_class.new) + end + instance_variable_get(:"@#{name}") + end + + define_method(:"#{name}=") do |value| + if value.is_a?(configuration_class) + instance_variable_set(:"@#{name}", value) + else + send(name).set(value) + end + end + end + + def option(name, type, default: nil, **args) + klass = "Alchemy::Configuration::#{type.to_s.camelize}Option".constantize + # The defined options on a class are all those defined directly on + # that class as well as those defined on ancestors. + # We store these as a class instance variable on each class which has a + # option. super() collects options defined on ancestors. + singleton_options = (@defined_singleton_options ||= []) + singleton_options << name.to_sym + + define_singleton_method :defined_options do + super() + singleton_options + end + + define_method(name) do + unless instance_variable_defined?(:"@#{name}") + send(:"#{name}=", default) + end + instance_variable_get(:"@#{name}").value + end + + define_method(:"#{name}=") do |value| + instance_variable_set(:"@#{name}", klass.new(value:, name:, **args)) + end + end + end + end +end diff --git a/lib/alchemy/configuration/base_option.rb b/lib/alchemy/configuration/base_option.rb new file mode 100644 index 0000000000..8baca65269 --- /dev/null +++ b/lib/alchemy/configuration/base_option.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Alchemy + class Configuration + class BaseOption + def self.value_class + raise NotImplementedError + end + + def initialize(value:, name:, **args) + @name = name + @value = validate(value) if value + end + attr_reader :name, :value + + private + + def validate(value) + raise TypeError, "#{name} must be set as a #{self.class.value_class.name}, given #{value.inspect}" unless value.is_a?(self.class.value_class) + value + end + end + end +end diff --git a/lib/alchemy/configuration/boolean_option.rb b/lib/alchemy/configuration/boolean_option.rb new file mode 100644 index 0000000000..f68838c6b5 --- /dev/null +++ b/lib/alchemy/configuration/boolean_option.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require "alchemy/configuration/base_option" + +module Alchemy + class Configuration + class BooleanOption < BaseOption + private + + def validate(value) + raise TypeError, "#{name} must be a Boolean, given #{value.inspect}" unless value.is_a?(TrueClass) || value.is_a?(FalseClass) + value + end + end + end +end diff --git a/lib/alchemy/configuration/class_option.rb b/lib/alchemy/configuration/class_option.rb new file mode 100644 index 0000000000..f85d43de8b --- /dev/null +++ b/lib/alchemy/configuration/class_option.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require "alchemy/configuration/base_option" + +module Alchemy + class Configuration + class ClassOption < BaseOption + def self.value_class + String + end + + def value = @value.constantize + end + end +end diff --git a/lib/alchemy/configuration/class_set_option.rb b/lib/alchemy/configuration/class_set_option.rb new file mode 100644 index 0000000000..a3900d4398 --- /dev/null +++ b/lib/alchemy/configuration/class_set_option.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require "alchemy/configuration/base_option" + +module Alchemy + class Configuration + class ClassSetOption < BaseOption + include Enumerable + + def value = self + + def <<(klass) + @value << klass.to_s + end + + def concat(klasses) + klasses.each do |klass| + self << klass + end + + self + end + + delegate :clear, :empty?, to: :@value + + def delete(object) + @value.delete(object.to_s) + end + + def each + @value.each do |klass| + yield klass.constantize + end + end + + def as_json = @value + + private + + def validate(value) + raise TypeError, "each #{name} must be set as a String" unless value.all? { _1.is_a?(String) } + value + end + end + end +end diff --git a/lib/alchemy/configuration/integer_list_option.rb b/lib/alchemy/configuration/integer_list_option.rb new file mode 100644 index 0000000000..a22c06866d --- /dev/null +++ b/lib/alchemy/configuration/integer_list_option.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require "alchemy/configuration/list_option" + +module Alchemy + class Configuration + class IntegerListOption < ListOption + def self.item_class + Integer + end + end + end +end diff --git a/lib/alchemy/configuration/integer_option.rb b/lib/alchemy/configuration/integer_option.rb new file mode 100644 index 0000000000..937f4d9999 --- /dev/null +++ b/lib/alchemy/configuration/integer_option.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require "alchemy/configuration/base_option" +module Alchemy + class Configuration + class IntegerOption < BaseOption + def self.value_class + Integer + end + end + end +end diff --git a/lib/alchemy/configuration/list_option.rb b/lib/alchemy/configuration/list_option.rb new file mode 100644 index 0000000000..c4a8b12954 --- /dev/null +++ b/lib/alchemy/configuration/list_option.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "alchemy/configuration/base_option" + +module Alchemy + class Configuration + class ListOption < BaseOption + def self.item_class + raise NotImplementedError + end + + private + + def validate(value) + unless value.is_a?(Array) && value.all? { _1.is_a?(self.class.item_class) } + raise TypeError, "#{@name} must be an Array of #{self.class.item_class.name.downcase.pluralize}, given #{value.inspect}" + end + value + end + end + end +end diff --git a/lib/alchemy/configuration/regexp_option.rb b/lib/alchemy/configuration/regexp_option.rb new file mode 100644 index 0000000000..05fa61b132 --- /dev/null +++ b/lib/alchemy/configuration/regexp_option.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Alchemy + class Configuration + class RegexpOption < BaseOption + def self.value_class + Regexp + end + end + end +end diff --git a/lib/alchemy/configuration/string_list_option.rb b/lib/alchemy/configuration/string_list_option.rb new file mode 100644 index 0000000000..6da4832660 --- /dev/null +++ b/lib/alchemy/configuration/string_list_option.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require "alchemy/configuration/list_option" + +module Alchemy + class Configuration + class StringListOption < ListOption + def self.item_class + String + end + end + end +end diff --git a/lib/alchemy/configuration/string_option.rb b/lib/alchemy/configuration/string_option.rb new file mode 100644 index 0000000000..0d3a44331f --- /dev/null +++ b/lib/alchemy/configuration/string_option.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Alchemy + class Configuration + class StringOption < BaseOption + def self.value_class + String + end + end + end +end diff --git a/lib/alchemy/configuration_methods.rb b/lib/alchemy/configuration_methods.rb index f64f26d495..9b5c9b3ab4 100644 --- a/lib/alchemy/configuration_methods.rb +++ b/lib/alchemy/configuration_methods.rb @@ -13,7 +13,7 @@ module ConfigurationMethods # Config file is in +config/alchemy/config.yml+ # def configuration(name) - Config.get(name) + Alchemy.config.get(name) end # Returns true if more than one language is published on current site. diff --git a/lib/alchemy/configurations/default_language.rb b/lib/alchemy/configurations/default_language.rb new file mode 100644 index 0000000000..c867a2295b --- /dev/null +++ b/lib/alchemy/configurations/default_language.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Alchemy + module Configurations + class DefaultLanguage < Alchemy::Configuration + option :name, :string, default: "English" + option :code, :string, default: "en" + option :page_layout, :string, default: "index" + option :frontpage_name, :string, default: "Index" + end + end +end diff --git a/lib/alchemy/configurations/default_site.rb b/lib/alchemy/configurations/default_site.rb new file mode 100644 index 0000000000..8539096e87 --- /dev/null +++ b/lib/alchemy/configurations/default_site.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Alchemy + module Configurations + class DefaultSite < Alchemy::Configuration + option :name, :string, default: "Default Site" + option :host, :string, default: "*" + end + end +end diff --git a/lib/alchemy/configurations/format_matchers.rb b/lib/alchemy/configurations/format_matchers.rb new file mode 100644 index 0000000000..438b77d27d --- /dev/null +++ b/lib/alchemy/configurations/format_matchers.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Alchemy + module Configurations + class FormatMatchers < Alchemy::Configuration + option :email, :regexp, default: /\A[^@\s]+@([^@\s]+\.)+[^@\s]+\z/ + option :url, :regexp, default: /\A[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?\z/ix + option :link_url, :regexp, default: /^(tel:|mailto:|\/|[a-z]+:\/\/)/ + end + end +end diff --git a/lib/alchemy/configurations/mailer.rb b/lib/alchemy/configurations/mailer.rb new file mode 100644 index 0000000000..05814d21dc --- /dev/null +++ b/lib/alchemy/configurations/mailer.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Alchemy + module Configurations + class Mailer < Alchemy::Configuration + option :page_layout_name, :string, default: "contact" + option :forward_to_page, :boolean, default: false + option :mail_success_page, :string, default: "thanks" + option :mail_from, :string, default: "your.mail@your-domain.com" + option :mail_to, :string, default: "your.mail@your-domain.com" + option :subject, :string, default: "A new contact form message" + option :fields, :string_list, default: %w[salutation firstname lastname address zip city phone email message] + option :validate_fields, :string_list, default: %w[lastname email] + end + end +end diff --git a/lib/alchemy/configurations/main.rb b/lib/alchemy/configurations/main.rb new file mode 100644 index 0000000000..260def8d11 --- /dev/null +++ b/lib/alchemy/configurations/main.rb @@ -0,0 +1,199 @@ +# frozen_string_literal: true + +require "alchemy/configuration" +require "alchemy/configurations/default_language" +require "alchemy/configurations/default_site" +require "alchemy/configurations/format_matchers" +require "alchemy/configurations/mailer" +require "alchemy/configurations/preview" +require "alchemy/configurations/sitemap" +require "alchemy/configurations/uploader" +require "alchemy/deprecation" + +module Alchemy + module Configurations + class Main < Alchemy::Configuration + # == This is the global Alchemy configuration class + + # === Auto Log Out Time + # + # The amount of time of inactivity in minutes after which the user is kicked out of his current session. + # + # NOTE: This is only active in production environments + # + option :auto_logout_time, :integer, default: 30 + + # === Page caching + # + # Enable/Disable page caching globally. + # + # NOTE: You can enable/disable page caching for single Alchemy::PageLayouts in the page_layout.yml file. + # + option :cache_pages, :boolean, default: true + + # === Sitemap + # + # Alchemy creates a XML, Google compatible, sitemap for you. + # + # The url is: http://your-domain.tld/sitemap.xml + # + # ==== Config Options: + # + # show_root [Boolean] # Show language root page in sitemap? + # show_flag [Boolean] # Enables the Checkbox in Page#update overlay. So your customer can set the visibility of pages in the sitemap. + configuration :sitemap, Sitemap + + # === Default items per page in admin views + # + # In Alchemy's Admin, change how many items you would get shown per page by Kaminari + option :items_per_page, :integer, default: 15 + + # === Preview window URL configuration + # + # By default Alchemy uses its internal page preview renderer, + # but you can configure it to be any URL instead. + # + # Basic Auth is supported. + # + # preview: + # host: https://www.my-static-site.com + # auth: + # username: <%= ENV["BASIC_AUTH_USERNAME"] %> + # password: <%= ENV["BASIC_AUTH_PASSWORD"] %> + # + # Preview config per site is supported as well. + # + # preview: + # My site name: + # host: https://www.my-static-site.com + # auth: + # username: <%= ENV["BASIC_AUTH_USERNAME"] %> + # password: <%= ENV["BASIC_AUTH_PASSWORD"] %> + # + configuration :preview, Preview + + # === Picture rendering settings + # + # Alchemy uses Dragonfly to render images. Settings for image rendering are specific to elements and are defined in elements.yml + # + # Example: + # - name: some_element + # ingredients: + # - role: some_picture + # type: Picture + # settings: + # hint: true + # crop: true # turns on image cropping + # size: '500x500' # image will be cropped to this size + # + # See http://markevans.github.com/dragonfly for further info. + # + # ==== Global Options: + # + # output_image_quality [Integer] # If image gets rendered as JPG or WebP this is the quality setting for it. (Default 85) + # preprocess_image_resize [String] # Use this option to resize images to the given size when they are uploaded to the image library. Downsizing example: '1000x1000>' (Default nil) + # image_output_format [String] # The global image output format setting. (Default +original+) + # + # NOTE: You can always override the output format in the settings of your ingredients in elements.yml, I.E. {format: 'gif'} + # + option :output_image_quality, :integer, default: 85 + alias_method :output_image_jpg_quality, :output_image_quality + deprecate output_image_jpg_quality: :output_image_quality, deprecator: Alchemy::Deprecation + alias_method :output_image_jpg_quality=, :output_image_quality= + deprecate "output_image_jpg_quality=": :output_image_quality=, deprecator: Alchemy::Deprecation + option :preprocess_image_resize, :string + option :image_output_format, :string, default: "original" + + # This is used by the seeder to create the default site. + configuration :default_site, DefaultSite + + # This is the default language when seeding. + configuration :default_language, DefaultLanguage + + # === Mailer Settings: + # + # To send emails via contact forms, you can create your form fields here and set which fields are to be validated. + # + # === Validating fields: + # + # Pass the field name as a symbol and a message_id (will be translated) to :validate_fields: + # + # ==== Options: + # + # page_layout_name: [String] # A +Alchemy::PageLayout+ name. Used to render the contactform on a page with this layout. + # fields: [Array] # An Array of fieldnames. + # validate_fields: [Array] # An Array of fieldnames to be validated on presence. + # + # ==== Translating validation messages: + # + # The validation messages are passed through ::I18n.t so you can translate it in your language yml file. + # + # ==== Example: + # + # de: + # activemodel: + # attributes: + # alchemy/message: + # firstname: Vorname + # + configuration :mailer, Mailer + + # === User roles + # + # You can add own user roles. + # + # Further documentation for the auth system used please visit: + # + # https://github.com/ryanb/cancan/wiki + # + # ==== Translating User roles + # + # Userroles can be translated inside your the language yml file under: + # + # alchemy: + # user_roles: + # rolename: Name of the role + # + option :user_roles, :string_list, default: %w[member author editor admin] + + # === Uploader Settings + # + # upload_limit [Integer] # Set an amount of files upload limit of files which can be uploaded at once. Set 0 for unlimited. + # file_size_limit* [Integer] # Set a file size limit in mega bytes for a per file limit. + # + # *) Allow filetypes to upload. Pass * to allow all kind of files. + # + configuration :uploader, Uploader + + # === Link Target Options + # + # Values for the link target selectbox inside the page link overlay. + # The value gets attached as a data-link-target attribute to the link. + # + # == Example: + # + # Open all links set to overlay inside an jQuery UI Dialog Window. + # + # jQuery(a[data-link-target="overlay"]).dialog(); + # + option :link_target_options, :string_list, default: %w[blank] + + # === Format matchers + # + # Named aliases for regular expressions that can be used in various places. + # The most common use case is the format validation of ingredients, or attribute validations of your individual models. + # + # == Example: + # + # validates_format_of :url, with: Alchemy.config.get('format_matchers')['url'] + # + configuration :format_matchers, FormatMatchers + + # The layout used for rendering the +alchemy/admin/pages#show+ action. + option :admin_page_preview_layout, :string, default: "application" + + # The sizes for the preview size select in the page editor. + option :page_preview_sizes, :integer_list, default: [360, 640, 768, 1024, 1280, 1440] + end + end +end diff --git a/lib/alchemy/configurations/preview.rb b/lib/alchemy/configurations/preview.rb new file mode 100644 index 0000000000..4aacd92850 --- /dev/null +++ b/lib/alchemy/configurations/preview.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Alchemy + module Configurations + class Preview < Alchemy::Configuration + class PreviewAuth < Alchemy::Configuration + option :username, :string + option :password, :string + end + + attr_reader :per_site_configs + + def initialize(configuration = {}) + @per_site_configs = [] + configuration = configuration.with_indifferent_access + configuration.except(:host, :site_name, :auth).keys.each do |site_name| + @per_site_configs << Preview.new(configuration[site_name].merge(site_name: site_name)) + end + super(configuration.slice(:host, :site_name, :auth)) + end + + option :site_name, :string, default: "*" + option :host, :string + + configuration :auth, PreviewAuth + + def for_site(site) + per_site_configs.detect { _1.site_name == site.name } || self + end + end + end +end diff --git a/lib/alchemy/configurations/sitemap.rb b/lib/alchemy/configurations/sitemap.rb new file mode 100644 index 0000000000..cbee82252d --- /dev/null +++ b/lib/alchemy/configurations/sitemap.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Alchemy + module Configurations + class Sitemap < Alchemy::Configuration + option :show_root, :boolean, default: true + option :show_flag, :boolean, default: false + end + end +end diff --git a/lib/alchemy/configurations/uploader.rb b/lib/alchemy/configurations/uploader.rb new file mode 100644 index 0000000000..a96d7d96b0 --- /dev/null +++ b/lib/alchemy/configurations/uploader.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Alchemy + module Configurations + class Uploader < Alchemy::Configuration + class AllowedFileTypes < Alchemy::Configuration + option :alchemy_attachments, :string_list, default: ["*"] + option :alchemy_pictures, :string_list, default: %w[jpg jpeg gif png svg webp] + + def set(configuration_hash) + super(configuration_hash.transform_keys { transform_key(_1) }) + end + + def get(key) + super(transform_key(key)) + end + alias_method :[], :get + + private + + def transform_key(key) + key.to_s.tr("/", "_") + end + end + + # Number of files that can be uploaded at once + option :upload_limit, :integer, default: 50 + # Megabytes + option :file_size_limit, :integer, default: 100 + + configuration :allowed_filetypes, AllowedFileTypes + end + end +end diff --git a/lib/alchemy/engine.rb b/lib/alchemy/engine.rb index bc9ec3a537..305f4dcf55 100644 --- a/lib/alchemy/engine.rb +++ b/lib/alchemy/engine.rb @@ -97,6 +97,18 @@ class Engine < Rails::Engine end end + initializer "alchemy.config_yml" do |app| + config_directory = Rails.root.join("config", "alchemy") + main_config = config_directory.join("config.yml") + env_specific_config = config_directory.join("#{Rails.env}.config.yml") + if File.exist?(main_config) + Alchemy.config.set_from_yaml(main_config) + end + if File.exist?(env_specific_config) + Alchemy.config.set_from_yaml(env_specific_config) + end + end + config.after_initialize do if Alchemy.user_class ActiveSupport.on_load(:active_record) do diff --git a/lib/alchemy/seeder.rb b/lib/alchemy/seeder.rb index fa9e8828c6..adb93454ea 100644 --- a/lib/alchemy/seeder.rb +++ b/lib/alchemy/seeder.rb @@ -116,7 +116,7 @@ def create_page(draft, attributes = {}) # If no languages are present, create a default language based # on the host app's Alchemy configuration. def create_default_language! - default_language = Alchemy::Config.get(:default_language) + default_language = Alchemy.config.get(:default_language) if default_language Alchemy::Language.create!( name: default_language["name"], @@ -134,7 +134,7 @@ def create_default_language! end def create_default_site! - default_site = Alchemy::Config.get(:default_site) + default_site = Alchemy.config.get(:default_site) if default_site Alchemy::Site.create!(name: default_site["name"], host: default_site["host"]) else diff --git a/lib/alchemy/test_support/config_stubbing.rb b/lib/alchemy/test_support/config_stubbing.rb index 1057a80282..a00e62f5df 100644 --- a/lib/alchemy/test_support/config_stubbing.rb +++ b/lib/alchemy/test_support/config_stubbing.rb @@ -17,13 +17,7 @@ module ConfigStubbing # @param value [Object] The value you want to return instead of the original one # def stub_alchemy_config(key, value) - allow(Alchemy::Config).to receive(:get) do |arg| - if arg == key - value - else - Alchemy::Config.show[arg.to_s] - end - end + allow(Alchemy.config).to receive(key).and_return(value) end end end diff --git a/lib/alchemy/test_support/factories/language_factory.rb b/lib/alchemy/test_support/factories/language_factory.rb index a26fb5a830..85d0008d34 100644 --- a/lib/alchemy/test_support/factories/language_factory.rb +++ b/lib/alchemy/test_support/factories/language_factory.rb @@ -6,7 +6,7 @@ code { ::I18n.available_locales.first.to_s } default { true } frontpage_name { "Intro" } - page_layout { Alchemy::Config.get(:default_language)["page_layout"] } + page_layout { Alchemy.config.get(:default_language)["page_layout"] } public { true } diff --git a/lib/alchemy/test_support/factories/site_factory.rb b/lib/alchemy/test_support/factories/site_factory.rb index de5cb539fe..1cab21042a 100644 --- a/lib/alchemy/test_support/factories/site_factory.rb +++ b/lib/alchemy/test_support/factories/site_factory.rb @@ -8,8 +8,8 @@ trait :default do public { true } - name { Alchemy::Config.get(:default_site)["name"] } - host { Alchemy::Config.get(:default_site)["host"] } + name { Alchemy.config.get(:default_site)["name"] } + host { Alchemy.config.get(:default_site)["host"] } end trait :public do diff --git a/lib/alchemy/upgrader.rb b/lib/alchemy/upgrader.rb index a17d387caa..65df0e405b 100644 --- a/lib/alchemy/upgrader.rb +++ b/lib/alchemy/upgrader.rb @@ -7,24 +7,5 @@ class Upgrader extend Alchemy::Shell Dir["#{File.dirname(__FILE__)}/upgrader/*.rb"].sort.each { |f| require f } - - class << self - def copy_new_config_file - desc "Copy configuration file." - config_file = Rails.root.join("config/alchemy/config.yml") - default_config = File.join(File.dirname(__FILE__), "../../config/alchemy/config.yml") - if !File.exist? config_file - log "No configuration file found. Creating it." - FileUtils.cp default_config, Rails.root.join("config/alchemy/config.yml") - elsif FileUtils.identical? default_config, config_file - log "Configuration file already present.", :skip - else - log "Custom configuration file found." - FileUtils.cp default_config, Rails.root.join("config/alchemy/config.yml.defaults") - log "Copied new default configuration file." - todo "Check the default configuration file (./config/alchemy/config.yml.defaults) for new configuration options and insert them into your config file.", "Configuration has changed" - end - end - end end end diff --git a/lib/alchemy_cms.rb b/lib/alchemy_cms.rb index 89a56fa9de..861d8dfe04 100644 --- a/lib/alchemy_cms.rb +++ b/lib/alchemy_cms.rb @@ -25,7 +25,6 @@ require_relative "alchemy/admin/preview_url" require_relative "alchemy/auth_accessors" require_relative "alchemy/cache_digests/template_tracker" -require_relative "alchemy/config" require_relative "alchemy/configuration_methods" require_relative "alchemy/controller_actions" require_relative "alchemy/deprecation" diff --git a/lib/generators/alchemy/install/install_generator.rb b/lib/generators/alchemy/install/install_generator.rb index 4f18d252cb..5ea4658b7f 100644 --- a/lib/generators/alchemy/install/install_generator.rb +++ b/lib/generators/alchemy/install/install_generator.rb @@ -53,16 +53,16 @@ def mount install_tasks.inject_routes(options[:auto_accept]) end - def copy_config - copy_file "#{gem_config_path}/config.yml", app_config_path.join("alchemy", "config.yml") - end - def copy_yml_files %w[elements page_layouts menus].each do |file| template "#{__dir__}/templates/#{file}.yml.tt", app_config_path.join("alchemy", "#{file}.yml") end end + def copy_config_rb + template "#{__dir__}/templates/alchemy.rb.tt", app_config_path.join("initializers", "alchemy.rb") + end + def install_assets copy_file "custom.css", app_assets_path.join("stylesheets/alchemy/admin/custom.css") end diff --git a/lib/generators/alchemy/install/templates/alchemy.rb.tt b/lib/generators/alchemy/install/templates/alchemy.rb.tt new file mode 100644 index 0000000000..224c98ff34 --- /dev/null +++ b/lib/generators/alchemy/install/templates/alchemy.rb.tt @@ -0,0 +1,197 @@ +Alchemy.configure do |config| + <% default_config = Alchemy::Configurations::Main.new %> + # == This is the global Alchemy configuration file + # + + # === Auto Log Out Time + # + # The amount of time of inactivity in minutes after which the user is kicked out of his current session. + # + # NOTE: This is only active in production environments + # + # config.auto_logout_time = <%= default_config.auto_logout_time.inspect %> + + # === Page caching + # + # Enable/Disable page caching globally. + # + # NOTE: You can enable/disable page caching for single Alchemy::PageLayouts in the page_layout.yml file. + # + # config.cache_pages = <%= default_config.cache_pages.inspect %> + + # === Sitemap + # + # Alchemy creates a XML, Google compatible, sitemap for you. + # + # The url is: http://your-domain.tld/sitemap.xml + # + # ==== Config Options: + # + # show_root [Boolean] # Show language root page in sitemap? + # show_flag [Boolean] # Enables the Checkbox in Page#update overlay. So your customer can set the visibility of pages in the sitemap. + # + # config.sitemap.tap do |sitemap| + # sitemap.show_root = <%= default_config.sitemap.show_root.inspect %> + # sitemap.show_flag = <%= default_config.sitemap.show_flag.inspect %> + # end + + # === Default items per page in admin views + # + # In Alchemy's Admin, change how many items you would get shown per page by Kaminari + # config.items_per_page = <%= default_config.items_per_page %> + + # === Preview window URL configuration + # + # By default Alchemy uses its internal page preview renderer, + # but you can configure it to be any URL instead. + # + # Basic Auth is supported. + # + # config.preview = { + # host: https://www.my-static-site.com + # auth: + # username: <%%= ENV["BASIC_AUTH_USERNAME"] %%> + # password: <%%= ENV["BASIC_AUTH_PASSWORD"] %%> + # } + # Preview config per site is supported as well. + # + # config.preview = { + # My site name: + # host: https://www.my-static-site.com + # auth: + # username: <%%= ENV["BASIC_AUTH_USERNAME"] %%> + # password: <%%= ENV["BASIC_AUTH_PASSWORD"] %%> + # } + + # === Picture rendering settings + # + # Alchemy uses Dragonfly to render images. Settings for image rendering are specific to elements and are defined in elements.yml + # + # Example: + # - name: some_element + # ingredients: + # - role: some_picture + # type: Picture + # settings: + # hint: true + # crop: true # turns on image cropping + # size: '500x500' # image will be cropped to this size + # + # See http://markevans.github.com/dragonfly for further info. + # + # ==== Global Options: + # + # output_image_quality [Integer] # If image gets rendered as JPG or WebP this is the quality setting for it. (Default 85) + # preprocess_image_resize [String] # Use this option to resize images to the given size when they are uploaded to the image library. Downsizing example: '1000x1000>' (Default nil) + # image_output_format [String] # The global image output format setting. (Default +original+) + # + # NOTE: You can always override the output format in the settings of your ingredients in elements.yml, I.E. {format: 'gif'} + # + # config.output_image_quality = <%= default_config.output_image_quality %> + # config.preprocess_image_resize = <%= default_config.preprocess_image_resize.inspect %> + # config.image_output_format = <%= default_config.image_output_format.inspect %> + + # This is used by the seeder to create the default site. + # config.default_site.tap do |default_site| + # default_site.name = <%= default_config.default_site.name.inspect %> + # default_site.host = <%= default_config.default_site.host.inspect %> + # end + + # This is the default language when seeding. + # config.default_language.tap do |default_language| + # default_language.code = <%= default_config.default_language.code.inspect %> + # default_language.name = <%= default_config.default_language.name.inspect %> + # default_language.page_layout = <%= default_config.default_language.page_layout.inspect %> + # default_language.frontpage_name = <%= default_config.default_language.frontpage_name.inspect %> + # end + + # === Mailer Settings: + # + # To send emails via contact forms, you can create your form fields here and set which fields are to be validated. + # + # === Validating fields: + # + # Pass the field name as a symbol and a message_id (will be translated) to :validate_fields: + # + # ==== Options: + # + # page_layout_name: [String] # A +Alchemy::PageLayout+ name. Used to render the contactform on a page with this layout. + # fields: [Array] # An Array of fieldnames. + # validate_fields: [Array] # An Array of fieldnames to be validated on presence. + # + # ==== Translating validation messages: + # + # The validation messages are passed through ::I18n.t so you can translate it in your language yml file. + # + # ==== Example: + # + # de: + # activemodel: + # attributes: + # alchemy/message: + # firstname: Vorname + # + # config.mailer.tap do |mailer| + # mailer.page_layout_name = <%= default_config.mailer.page_layout_name.inspect %> + # mailer.forward_to_page = <%= default_config.mailer.forward_to_page.inspect %> + # mailer.mail_success_page = <%= default_config.mailer.mail_success_page.inspect %> + # mailer.mail_from = <%= default_config.mailer.mail_from.inspect %> + # mailer.mail_to = <%= default_config.mailer.mail_to.inspect %> + # mailer.subject = <%= default_config.mailer.subject.inspect %> + # mailer.fields = <%= default_config.mailer.fields.inspect %> + # mailer.validate_fields = <%= default_config.mailer.validate_fields.inspect %> + # end + + # === User roles + # + # You can add own user roles. + # + # Further documentation for the auth system used please visit: + # + # https://github.com/ryanb/cancan/wiki + # + # ==== Translating User roles + # + # Userroles can be translated inside your the language yml file under: + # + # alchemy: + # user_roles: + # rolename: Name of the role + # + # config.user_roles = <%= default_config.user_roles.inspect %> + + # === Uploader Settings + # + # upload_limit [Integer] # Set an amount of files upload limit of files which can be uploaded at once. Set 0 for unlimited. + # file_size_limit* [Integer] # Set a file size limit in mega bytes for a per file limit. + # + # *) Allow filetypes to upload. Pass * to allow all kind of files. + # + # config.uploader.tap do |uploader| + # uploader.upload_limit = <%= default_config.uploader.upload_limit.inspect %> + # uploader.file_size_limit = <%= default_config.uploader.file_size_limit.inspect %> + # uploader.allowed_filetypes.tap do |file_types| + # file_types.alchemy_attachments = <%= default_config.uploader.allowed_filetypes.alchemy_attachments.inspect %> + # file_types.alchemy_pictures = <%= default_config.uploader.allowed_filetypes.alchemy_pictures.inspect %> + # end + # end + + # === Link Target Options + # + # Values for the link target selectbox inside the page link overlay. + # The value gets attached as a data-link-target attribute to the link. + # + # == Example: + # + # Open all links set to overlay inside an jQuery UI Dialog Window. + # + # jQuery(a[data-link-target="overlay"]).dialog(); + # + # config.link_target_options = <%= default_config.link_target_options.inspect %> + + # The layout used for rendering the +alchemy/admin/pages#show+ action. + # config.admin_page_preview_layout = <%= default_config.admin_page_preview_layout.inspect %> + + # The sizes for the preview size select in the page editor. + # config.page_preview_sizes = <%= default_config.page_preview_sizes.inspect %> +end diff --git a/lib/generators/alchemy/install/templates/page_layouts.yml.tt b/lib/generators/alchemy/install/templates/page_layouts.yml.tt index 45a73a3907..cf727885ca 100644 --- a/lib/generators/alchemy/install/templates/page_layouts.yml.tt +++ b/lib/generators/alchemy/install/templates/page_layouts.yml.tt @@ -3,7 +3,7 @@ # For further information please see https://guides.alchemy-cms.com/pages.html <%- unless @options[:skip_demo_files] -%> -- name: <%= Alchemy::Config.get(:default_language)['page_layout'] %> +- name: <%= Alchemy.config.get(:default_language)['page_layout'] %> unique: true elements: [article] autogenerate: [article] diff --git a/lib/tasks/alchemy/upgrade.rake b/lib/tasks/alchemy/upgrade.rake index de7fa571fc..c9f971a5d1 100644 --- a/lib/tasks/alchemy/upgrade.rake +++ b/lib/tasks/alchemy/upgrade.rake @@ -23,10 +23,5 @@ namespace :alchemy do "alchemy:install:migrations", "db:migrate" ] - - desc "Alchemy Upgrader: Copy configuration file." - task config: [:environment] do - Alchemy::Upgrader.copy_new_config_file - end end end diff --git a/spec/controllers/alchemy/admin/languages_controller_spec.rb b/spec/controllers/alchemy/admin/languages_controller_spec.rb index 7892287e32..9b43456d48 100644 --- a/spec/controllers/alchemy/admin/languages_controller_spec.rb +++ b/spec/controllers/alchemy/admin/languages_controller_spec.rb @@ -65,7 +65,7 @@ it "has default language's page_layout set" do get :new expect(assigns(:language).page_layout) - .to eq(Alchemy::Config.get(:default_language)["page_layout"]) + .to eq(Alchemy.config.get(:default_language)["page_layout"]) end end end diff --git a/spec/controllers/alchemy/base_controller_spec.rb b/spec/controllers/alchemy/base_controller_spec.rb index 505b1c01a4..6175a73cc0 100644 --- a/spec/controllers/alchemy/base_controller_spec.rb +++ b/spec/controllers/alchemy/base_controller_spec.rb @@ -29,7 +29,7 @@ module Alchemy describe "#configuration" do it "returns certain configuration options" do - allow(Config).to receive(:show).and_return({"some_option" => true}) + allow(Config).to receive(:some_option).and_return(true) expect(controller.configuration(:some_option)).to eq(true) end end diff --git a/spec/controllers/alchemy/messages_controller_spec.rb b/spec/controllers/alchemy/messages_controller_spec.rb index 26aa25004c..e996c1664b 100644 --- a/spec/controllers/alchemy/messages_controller_spec.rb +++ b/spec/controllers/alchemy/messages_controller_spec.rb @@ -265,7 +265,7 @@ module Alchemy context "when mailer_config['fields'] does not inlcude :contact_field_id" do it "should permit :contact_form_id" do - expect(Alchemy::Config.get(:mailer)["fields"]).not_to include "contact_field_id" + expect(Alchemy.config.get(:mailer)["fields"]).not_to include "contact_field_id" msg_params = subject.send(:message_params) expect(msg_params).to include :contact_form_id diff --git a/spec/dummy/config/alchemy/config.yml b/spec/dummy/config/alchemy/config.yml new file mode 100644 index 0000000000..7c38b87918 --- /dev/null +++ b/spec/dummy/config/alchemy/config.yml @@ -0,0 +1,2 @@ +output_image_quality: 90 +auto_logout_time: 40 diff --git a/spec/dummy/config/alchemy/elements.yml b/spec/dummy/config/alchemy/elements.yml index 22c5455f42..c7e7045390 100644 --- a/spec/dummy/config/alchemy/elements.yml +++ b/spec/dummy/config/alchemy/elements.yml @@ -67,7 +67,7 @@ - role: mail_from type: Text validate: - - presence + - format: email - role: mail_to type: Text validate: diff --git a/spec/dummy/config/alchemy/test.config.yml b/spec/dummy/config/alchemy/test.config.yml new file mode 100644 index 0000000000..6dab462ebd --- /dev/null +++ b/spec/dummy/config/alchemy/test.config.yml @@ -0,0 +1 @@ +output_image_quality: 85 diff --git a/spec/fixtures/config.yml b/spec/fixtures/config.yml new file mode 100644 index 0000000000..4e828e9742 --- /dev/null +++ b/spec/fixtures/config.yml @@ -0,0 +1 @@ +auto_logout_time: 20 diff --git a/spec/libraries/admin/preview_url_spec.rb b/spec/libraries/admin/preview_url_spec.rb index 2d68e10bd8..3d7b9f8636 100644 --- a/spec/libraries/admin/preview_url_spec.rb +++ b/spec/libraries/admin/preview_url_spec.rb @@ -21,9 +21,9 @@ context "with preview configured" do context "without protocol" do before do - stub_alchemy_config(:preview, { + stub_alchemy_config(:preview, Alchemy::Configurations::Preview.new({ "host" => "www.example.com" - }) + })) end it "raises error" do @@ -33,9 +33,9 @@ context "as http url" do before do - stub_alchemy_config(:preview, { + stub_alchemy_config(:preview, Alchemy::Configurations::Preview.new({ "host" => "http://www.example.com" - }) + })) end it "returns the configured preview url" do @@ -45,9 +45,9 @@ context "as https url" do before do - stub_alchemy_config(:preview, { + stub_alchemy_config(:preview, Alchemy::Configurations::Preview.new({ "host" => "https://www.example.com" - }) + })) end it "returns the configured preview url with https" do @@ -57,13 +57,13 @@ context "and with basic auth configured" do before do - stub_alchemy_config(:preview, { + stub_alchemy_config(:preview, Alchemy::Configurations::Preview.new({ "host" => "https://www.example.com", "auth" => { "username" => "foo", "password" => "baz" } - }) + })) end it "returns the configured preview url with userinfo" do @@ -73,9 +73,9 @@ context "with a port configured" do before do - stub_alchemy_config(:preview, { + stub_alchemy_config(:preview, Alchemy::Configurations::Preview.new({ "host" => "https://www.example.com:8080" - }) + })) end it "returns the configured preview url with userinfo" do @@ -90,11 +90,11 @@ context "that matches the pages site name" do let(:config) do - { + Alchemy::Configurations::Preview.new({ page.site.name => { "host" => "http://new.example.com" } - } + }) end it "returns the configured preview url for that site" do @@ -105,12 +105,12 @@ context "that does not match the pages site name" do context "with a default configured" do let(:config) do - { + Alchemy::Configurations::Preview.new({ "Not matching site name" => { "host" => "http://new.example.com" }, "host" => "http://www.example.com" - } + }) end it "returns the default configured preview url" do @@ -120,11 +120,11 @@ context "without a default configured" do let(:config) do - { + Alchemy::Configurations::Preview.new({ "Not matching site name" => { "host" => "http://new.example.com" } - } + }) end it "returns the internal preview url" do @@ -138,9 +138,9 @@ let(:page) { create(:alchemy_page, :language_root) } before do - stub_alchemy_config(:preview, { + stub_alchemy_config(:preview, Alchemy::Configurations::Preview.new({ "host" => "https://www.example.com" - }) + })) end it "returns the preview url without urlname" do diff --git a/spec/libraries/alchemy/configuration/class_set_option_spec.rb b/spec/libraries/alchemy/configuration/class_set_option_spec.rb new file mode 100644 index 0000000000..bb2473ea6c --- /dev/null +++ b/spec/libraries/alchemy/configuration/class_set_option_spec.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +require "rails_helper" +require "alchemy/configuration/class_set_option" + +module ClassSetTest + ClassA = Class.new + ClassB = Class.new + + def self.reload + [:ClassA, :ClassB].each do |klass| + remove_const(klass) + const_set(klass, Class.new) + end + end +end + +RSpec.describe Alchemy::Configuration::ClassSetOption do + let(:set) { described_class.new(value: [], name: :my_class_set) } + + describe "#concat" do + it "can add one item" do + set.concat(["ClassSetTest::ClassA"]) + expect(set).to include(ClassSetTest::ClassA) + end + + it "can add two items" do + set.concat(["ClassSetTest::ClassA", ClassSetTest::ClassB]) + expect(set).to include(ClassSetTest::ClassA) + expect(set).to include(ClassSetTest::ClassB) + end + + it "returns itself" do + expect(set.concat(["String"])).to eql(set) + end + end + + describe "initializing with a default" do + let(:set) { described_class.new(value: ["ClassSetTest::ClassA"], name: :my_class_set) } + + it "contains the default" do + expect(set).to include(ClassSetTest::ClassA) + end + end + + describe "<<" do + it "can add by string" do + set << "ClassSetTest::ClassA" + expect(set).to include(ClassSetTest::ClassA) + end + + it "can add by class" do + set << ClassSetTest::ClassA + expect(set).to include(ClassSetTest::ClassA) + end + + describe "class redefinition" do + shared_examples "working code reloading" do + it "works with a class" do + original = ClassSetTest::ClassA + + ClassSetTest.reload + + # Sanity check + expect(original).not_to eq(ClassSetTest::ClassA) + + expect(set).to include(ClassSetTest::ClassA) + expect(set).to_not include(original) + end + end + + context "with a class" do + before { set << ClassSetTest::ClassA } + it_should_behave_like "working code reloading" + end + + context "with a string" do + before { set << "ClassSetTest::ClassA" } + it_should_behave_like "working code reloading" + end + end + end + + describe "#delete" do + before do + set << ClassSetTest::ClassA + end + + it "can delete by string" do + set.delete "ClassSetTest::ClassA" + expect(set).not_to include(ClassSetTest::ClassA) + end + + it "can delete by class" do + set.delete ClassSetTest::ClassA + expect(set).not_to include(ClassSetTest::ClassA) + end + end +end diff --git a/spec/libraries/alchemy/configuration/list_option_spec.rb b/spec/libraries/alchemy/configuration/list_option_spec.rb new file mode 100644 index 0000000000..4cc81134be --- /dev/null +++ b/spec/libraries/alchemy/configuration/list_option_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require "rails_helper" +require "alchemy/configuration/list_option" + +RSpec.describe Alchemy::Configuration::ListOption do + describe ".item_class" do + subject { described_class.item_class } + + it "raises NotImplementedError" do + expect { subject }.to raise_exception(NotImplementedError) + end + end +end diff --git a/spec/libraries/alchemy/configuration_spec.rb b/spec/libraries/alchemy/configuration_spec.rb new file mode 100644 index 0000000000..0ccae5e33b --- /dev/null +++ b/spec/libraries/alchemy/configuration_spec.rb @@ -0,0 +1,273 @@ +# frozen_string_literal: true + +require "rails_helper" +require "alchemy/configuration" + +RSpec.describe Alchemy::Configuration do + let(:configuration) do + Class.new(described_class) do + option :auto_logout_time, :integer, default: 30 + end.new + end + + it "has a setter" do + expect do + configuration.auto_logout_time = 15 + end.to change { configuration.auto_logout_time }.from(30).to(15) + end + + it "allows setting the option to nil" do + expect do + configuration.auto_logout_time = nil + end.to change { configuration.auto_logout_time }.from(30).to(nil) + end + + it "has several getters" do + expect(configuration["auto_logout_time"]).to eq(30) + expect(configuration.auto_logout_time).to eq(30) + expect(configuration.get(:auto_logout_time)).to eq(30) + end + + it "returns self when #show is called" do + expect(configuration.show).to eq(configuration) + end + + context "without a default value" do + let(:configuration) do + Class.new(described_class) do + option :auto_logout_time, :integer + end.new + end + + it "defaults to nil" do + expect(configuration.auto_logout_time).to be nil + end + + describe "#fetch" do + it "allows getting data but setting a default, like Hash" do + expect(configuration.fetch("auto_logout_time", 20)).to eq(20) + configuration.auto_logout_time = 40 + expect(configuration.fetch("auto_logout_time", 20)).to eq(40) + end + end + end + + context "setting with the wrong type" do + it "raises an error" do + expect do + configuration.auto_logout_time = "14" + end.to raise_exception(TypeError, 'auto_logout_time must be set as a Integer, given "14"') + end + end + + describe "setting classes" do + let(:configuration) do + Class.new(described_class) do + option :picture_thumb_storage_class, :class, default: "Alchemy::PictureThumb::FileStore" + end.new + end + + it "returns the constantized class" do + expect(configuration.picture_thumb_storage_class).to be Alchemy::PictureThumb::FileStore + end + + it "can only be set using a string" do + expect do + configuration.picture_thumb_storage_class = String + end.to raise_exception(TypeError, "picture_thumb_storage_class must be set as a String, given String") + end + end + + describe "setting and changing class sets" do + let(:configuration) do + Class.new(described_class) do + option :preview_sources, :class_set, default: ["Alchemy::Admin::PreviewUrl"] + end.new + end + + it "returns an Enumerable that returns all classes as constants" do + expect(configuration.preview_sources.to_a).to eq([Alchemy::Admin::PreviewUrl]) + end + end + + describe "Boolean options" do + let(:configuration) do + Class.new(described_class) do + option :cache_pages, :boolean, default: true + end.new + end + + it "returns the boolean" do + expect(configuration.cache_pages).to be true + end + + it "can only be set with a Boolean" do + expect do + configuration.cache_pages = "true" + end.to raise_exception(TypeError, "cache_pages must be a Boolean, given \"true\"") + end + end + + describe "integer lists" do + let(:configuration) do + Class.new(described_class) do + option :page_preview_sizes, :integer_list, default: [1, 2] + end.new + end + + it "returns the integer list" do + expect(configuration.page_preview_sizes).to eq([1, 2]) + end + + it "can only be set with an integer list" do + expect do + configuration.page_preview_sizes = ["1"] + end.to raise_exception(TypeError, 'page_preview_sizes must be an Array of integers, given ["1"]') + end + end + + describe "string lists" do + let(:configuration) do + Class.new(described_class) do + option :link_target_options, :string_list, default: ["blank"] + end.new + end + + it "returns the string list" do + expect(configuration.link_target_options).to eq(["blank"]) + end + + it "can only be set with an Array of strings" do + expect do + configuration.link_target_options = [:blank] + end.to raise_exception(TypeError, "link_target_options must be an Array of strings, given [:blank]") + end + end + + describe "string options" do + let(:configuration) do + Class.new(described_class) do + option :mail_success_page, :string, default: "thanks" + end.new + end + + it "returns the string list" do + expect(configuration.mail_success_page).to eq("thanks") + end + + it "can only be set with an Array of strings" do + expect do + configuration.mail_success_page = :thanks + end.to raise_exception(TypeError, "mail_success_page must be set as a String, given :thanks") + end + end + + describe "regexp options" do + let(:configuration) do + Class.new(described_class) do + option :email, :regexp, default: /\A.*\z/ + end.new + end + + it "returns the regexp" do + expect(configuration.email).to eq(/\A.*\z/) + end + + it "can only be set with a regexp" do + expect do + configuration.email = '/\A.*\z/' + end.to raise_exception(TypeError, /email must be set as a Regexp, given .*/) + end + end + + describe "initializing with a hash" do + let(:configuration_class) do + Class.new(described_class) do + option :mail_success_page, :string, default: "thanks" + option :link_target_options, :string_list, default: ["blank"] + end + end + + let(:configuration) do + configuration_class.new(mail_success_page: "verymuchthankyou", link_target_options: ["top"]) + end + + it "takes the values from the hash" do + expect(configuration.mail_success_page).to eq("verymuchthankyou") + expect(configuration.link_target_options).to eq(["top"]) + end + end + + describe "#set" do + let(:configuration_class) do + Class.new(described_class) do + option :mail_success_page, :string, default: "thanks" + option :link_target_options, :string_list, default: ["blank"] + end + end + + let(:configuration) do + configuration_class.new + end + + it "takes the values from the hash" do + configuration.set(mail_success_page: "verymuchthankyou", link_target_options: ["top"]) + expect(configuration.mail_success_page).to eq("verymuchthankyou") + expect(configuration.link_target_options).to eq(["top"]) + end + end + + describe "get" do + let(:configuration) do + Class.new(described_class) do + option :mail_success_page, :string, default: "thanks" + option :link_target_options, :string_list, default: ["blank"] + end.new + end + + subject { configuration.get(:mail_success_page) } + + it { is_expected.to eq("thanks") } + + it "can be converted to a Hash" do + expect(configuration.to_h).to eq( + mail_success_page: "thanks", + link_target_options: ["blank"] + ) + end + end + + describe "nested configurations" do + let(:configuration) do + uploader_configuration_class = Class.new(described_class) do + option :file_size_limit, :integer, default: 100 + option :upload_limit, :integer, default: 50 + end + + Class.new(described_class) do + configuration :uploader, uploader_configuration_class + end.new + end + + it "can be accessed as methods" do + expect(configuration.uploader.upload_limit).to eq(50) + end + + it "can be accessed with []" do + expect(configuration[:uploader][:upload_limit]).to eq(50) + end + + it "can be set using a nested hash" do + configuration.set(uploader: {upload_limit: 30}) + configuration.set(uploader: {file_size_limit: 80}) + expect(configuration.uploader.upload_limit).to eq(30) + expect(configuration.uploader.file_size_limit).to eq(80) + end + + it "can be converted to a Hash" do + expect(configuration.to_h).to eq( + uploader: {file_size_limit: 100, upload_limit: 50} + ) + end + end +end diff --git a/spec/libraries/alchemy/configurations/main_spec.rb b/spec/libraries/alchemy/configurations/main_spec.rb new file mode 100644 index 0000000000..a2b1253a5f --- /dev/null +++ b/spec/libraries/alchemy/configurations/main_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require "rails_helper" +require "alchemy/configurations/main" + +RSpec.describe Alchemy::Configurations::Main do + describe "#set_from_yaml" do + let(:fixture_file) do + Rails.root.join("..", "fixtures", "config.yml") + end + + subject { described_class.new } + + before { subject.set_from_yaml(fixture_file) } + + it "has data from the yaml file" do + expect(subject.auto_logout_time).to eq(20) + end + end + + describe "deprecated: #output_image_jpg_quality getter" do + it "warns and tells us about the right method" do + expect(Alchemy::Deprecation).to receive(:warn).at_least(:once) + expect(subject.output_image_jpg_quality).to eq(85) + end + end + + describe "deprecated: #output_image_jpg_quality setter" do + it "warns and tells us about the right method" do + expect(Alchemy::Deprecation).to receive(:warn).at_least(:once) + expect do + subject.output_image_jpg_quality = 90 + end.to change { subject.output_image_quality }.from(85).to(90) + end + end +end diff --git a/spec/libraries/alchemy_spec.rb b/spec/libraries/alchemy_spec.rb index ff3d2f6abb..ec8ced94a9 100644 --- a/spec/libraries/alchemy_spec.rb +++ b/spec/libraries/alchemy_spec.rb @@ -35,4 +35,78 @@ end end end + + describe ".config" do + subject { Alchemy.config } + + it "is a config object" do + expect(subject).to be_a(Alchemy::Configurations::Main) + end + + it "has the auto_logout_time from config.yml" do + expect(subject.auto_logout_time).to eq(40) + end + + it "has the output_image_quality from test.config.yml" do + expect(subject.output_image_quality).to eq(85) + end + + it "has the default image output format" do + expect(subject.image_output_format).to eq("original") + end + + describe "format matchers" do + describe "email" do + subject { Alchemy.config.get("format_matchers")["email"] } + + it { is_expected.to match("hello@gmail.com") } + it { is_expected.not_to match("stulli@gmx") } + end + + describe "url" do + subject { Alchemy.config.get("format_matchers")["url"] } + + it { is_expected.to match("www.example.com:80/about") } + it { is_expected.not_to match('www.example.com:80\/about') } + end + + describe "link_url" do + subject { Alchemy.config.get("format_matchers")["link_url"] } + + it { is_expected.to match("tel:12345") } + it { is_expected.to match("mailto:stulli@gmx.de") } + it { is_expected.to match("/home") } + it { is_expected.to match("https://example.com/home") } + it { is_expected.not_to match('\/brehmstierleben') } + it { is_expected.not_to match('https:\/\/example.com/home') } + end + end + end + + describe ".configure" do + subject do + Alchemy.configure do |config| + config.auto_logout_time = 500 + end + end + + around do |example| + old_auto_logout_time = Alchemy.config.auto_logout_time + example.run + Alchemy.config.auto_logout_time = old_auto_logout_time + end + + it "yields the config object" do + expect { subject }.to change(Alchemy.config, :auto_logout_time).to(500) + end + end + + describe "deprecated: Config" do + subject { Alchemy::Config } + + it "is the same as Alchemy.config, but deprecated" do + expect(Alchemy::Deprecation).to receive(:warn) + expect(Alchemy::Config).to eq(Alchemy.config) + end + end end diff --git a/spec/libraries/config_spec.rb b/spec/libraries/config_spec.rb deleted file mode 100644 index 45a03c37b2..0000000000 --- a/spec/libraries/config_spec.rb +++ /dev/null @@ -1,209 +0,0 @@ -# frozen_string_literal: true - -require "rails_helper" - -module Alchemy - describe Config do - describe ".get" do - it "should call #show" do - expect(Config).to receive(:show).and_return({}) - Config.get(:mailer) - end - - it "should return the requested part of the config" do - expect(Config).to receive(:show).and_return({"mailer" => {"setting" => "true"}}) - expect(Config.get(:mailer)).to eq({"setting" => "true"}) - end - - context "if config is deprecated" do - context "without a new default" do - before do - expect(described_class).to receive(:deprecated_configs).at_least(:once) do - {foo: nil} - end - end - - it "warns" do - expect(Alchemy::Deprecation).to receive(:warn) - Config.get(:foo) - end - end - - context "with a new default" do - before do - expect(described_class).to receive(:deprecated_configs).at_least(:once) do - {foo: true} - end - end - - context "and current value is not the default" do - before do - expect(described_class).to receive(:show).at_least(:once) do - {"foo" => false} - end - end - - it "warns about new default" do - expect(Alchemy::Deprecation).to \ - receive(:warn).with("Setting foo configuration to false is deprecated and will be always true in Alchemy #{Alchemy::Deprecation.deprecation_horizon}") - Config.get(:foo) - end - end - end - end - - context "if config has been replaced" do - context "with a new default" do - before do - expect(described_class).to receive(:replaced_config_keys).at_least(:once) do - {foo: :bar} - end - end - - context "and config uses old key" do - before do - expect(described_class).to receive(:show).at_least(:once) do - {"bar" => :baz} - end - end - - it "warns about new key and returns old value" do - expect(Alchemy::Deprecation).to \ - receive(:warn).with("Using bar configuration is deprecated and will be removed in Alchemy #{Alchemy::Deprecation.deprecation_horizon}. Please use foo instead.") - expect(Config.get(:foo)).to eq(:baz) - end - end - - context "and config uses new key" do - before do - expect(described_class).to receive(:show).at_least(:once) do - {"foo" => :bar} - end - end - - it "warns about new key and returns old value" do - expect(Alchemy::Deprecation).to_not receive(:warn) - expect(Config.get(:foo)).to eq(:bar) - end - end - end - end - end - - describe ".main_app_config" do - let(:main_app_config_path) { Rails.root.join("config/alchemy/config.yml") } - - it "should call and return .read_file with the correct config path" do - expect(Config).to receive(:read_file).with(main_app_config_path).once.and_return({setting: "true"}) - expect(Config.send(:main_app_config)).to eq({setting: "true"}) - end - end - - describe ".env_specific_config" do - let(:env_specific_config_path) { Rails.root.join("config/alchemy/#{Rails.env}.config.yml") } - - it "should call and return .read_file with the correct config path" do - expect(Config).to receive(:read_file).with(env_specific_config_path).once.and_return({setting: "true"}) - expect(Config.send(:env_specific_config)).to eq({setting: "true"}) - end - end - - describe ".show" do - context "when ivar @config was not set before" do - before { Config.instance_variable_set(:@config, nil) } - - it "should call and return .merge_configs!" do - expect(Config).to receive(:merge_configs!).once.and_return({setting: "true"}) - expect(Config.show).to eq({setting: "true"}) - end - end - - context "when ivar @config was already set" do - before { Config.instance_variable_set(:@config, {setting: "true"}) } - after { Config.instance_variable_set(:@config, nil) } - - it "should have memoized the return value of .merge_configs!" do - expect(Config.send(:show)).to eq({setting: "true"}) - end - end - end - - describe ".read_file" do - context "when given path to yml file exists" do - context "and file is empty" do - before do - # YAML.safe_load returns nil if file is empty. - allow(YAML).to receive(:safe_load) { nil } - end - - it "should return an empty Hash" do - expect(Config.send(:read_file, "empty_file.yml")).to eq({}) - end - end - end - - context "when given path to yml file does not exist" do - it "should return an empty Hash" do - expect(Config.send(:read_file, "does/not/exist.yml")).to eq({}) - end - end - end - - describe ".merge_configs!" do - let(:config_1) do - {setting_1: "same", other_setting: "something"} - end - - let(:config_2) do - {setting_1: "same", setting_2: "anything"} - end - - it "should stringify the keys" do - expect(Config.send(:merge_configs!, config_1)).to eq(config_1.stringify_keys!) - end - - context "when all passed configs are empty" do - it "should raise an error" do - expect { Config.send(:merge_configs!, {}) }.to raise_error(LoadError) - end - end - - context "when configs containing same keys" do - it "should merge them together" do - expect(Config.send(:merge_configs!, config_1, config_2)).to eq( - "setting_1" => "same", - "other_setting" => "something", - "setting_2" => "anything" - ) - end - end - end - - describe "format matchers" do - describe "email" do - subject { Alchemy::Config.get("format_matchers")["email"] } - - it { is_expected.to match("hello@gmail.com") } - it { is_expected.not_to match("stulli@gmx") } - end - - describe "url" do - subject { Alchemy::Config.get("format_matchers")["url"] } - - it { is_expected.to match("www.example.com:80/about") } - it { is_expected.not_to match('www.example.com:80\/about') } - end - - describe "link_url" do - subject { Alchemy::Config.get("format_matchers")["link_url"] } - - it { is_expected.to match("tel:12345") } - it { is_expected.to match("mailto:stulli@gmx.de") } - it { is_expected.to match("/home") } - it { is_expected.to match("https://example.com/home") } - it { is_expected.not_to match('\/brehmstierleben') } - it { is_expected.not_to match('https:\/\/example.com/home') } - end - end - end -end diff --git a/spec/models/alchemy/ingredient_validator_spec.rb b/spec/models/alchemy/ingredient_validator_spec.rb index 5e19eaca75..8b3e32f5d3 100644 --- a/spec/models/alchemy/ingredient_validator_spec.rb +++ b/spec/models/alchemy/ingredient_validator_spec.rb @@ -56,6 +56,23 @@ it { expect(ingredient.errors).to be_present } end + + context "an element with email format validation" do + let(:element) { create(:alchemy_element, :with_ingredients, name: "contactform") } + let(:ingredient) { element.ingredient_by_role(:mail_from) } + + context "and the value is matching" do + let(:value) { "my_email@example.com" } + + it { expect(ingredient.errors).to be_blank } + end + + context "and the value is not matching" do + let(:value) { "my_email@example" } + + it { expect(ingredient.errors).to be_present } + end + end end context "an element with url format validation" do diff --git a/spec/models/alchemy/message_spec.rb b/spec/models/alchemy/message_spec.rb index 70ebcd7b98..a558858ed1 100644 --- a/spec/models/alchemy/message_spec.rb +++ b/spec/models/alchemy/message_spec.rb @@ -7,12 +7,12 @@ describe ".config" do it "should return the mailer config" do - expect(Alchemy::Message.config).to eq(Alchemy::Config.get(:mailer)) + expect(Alchemy::Message.config).to eq(Alchemy.config.get(:mailer)) end end it "has attributes writers and getters for all fields defined in mailer config" do - Alchemy::Config.get(:mailer)["fields"].each do |field| + Alchemy.config.get(:mailer)["fields"].each do |field| expect(message).to respond_to(field) expect(message).to respond_to("#{field}=") end @@ -21,7 +21,7 @@ context "validation of" do context "all fields defined in mailer config" do it "adds errors on that fields" do - Alchemy::Config.get(:mailer)["validate_fields"].each do |field| + Alchemy.config.get(:mailer)["validate_fields"].each do |field| expect(message).to_not be_valid expect(message.errors[field].size).to eq(1) end diff --git a/spec/requests/alchemy/admin/pages_controller_spec.rb b/spec/requests/alchemy/admin/pages_controller_spec.rb index 8ee33af0df..775ac74489 100644 --- a/spec/requests/alchemy/admin/pages_controller_spec.rb +++ b/spec/requests/alchemy/admin/pages_controller_spec.rb @@ -296,9 +296,7 @@ module Alchemy context "when layout is set to custom" do before do - allow(Alchemy::Config).to receive(:get) do |arg| - (arg == :admin_page_preview_layout) ? "custom" : Alchemy::Config.parameter(arg) - end + stub_alchemy_config(:admin_page_preview_layout, "custom") end it "it renders custom layout instead" do