diff --git a/app/models/alchemy/element.rb b/app/models/alchemy/element.rb index ebc02e0060..800c2f663d 100644 --- a/app/models/alchemy/element.rb +++ b/app/models/alchemy/element.rb @@ -20,6 +20,11 @@ # parent_element_id :integer # +require_dependency "alchemy/element/definitions" +require_dependency "alchemy/element/element_contents" +require_dependency "alchemy/element/element_essences" +require_dependency "alchemy/element/presenters" + module Alchemy class Element < BaseRecord NAME_REGEXP = /\A[a-z0-9_-]+\z/ @@ -117,10 +122,10 @@ class Element < BaseRecord delegate :restricted?, to: :page, allow_nil: true # Concerns - include Alchemy::Element::Definitions - include Alchemy::Element::ElementContents - include Alchemy::Element::ElementEssences - include Alchemy::Element::Presenters + include Definitions + include ElementContents + include ElementEssences + include Presenters # class methods class << self diff --git a/app/models/alchemy/element/definitions.rb b/app/models/alchemy/element/definitions.rb index 91b98af3e7..3455b8d599 100644 --- a/app/models/alchemy/element/definitions.rb +++ b/app/models/alchemy/element/definitions.rb @@ -1,37 +1,39 @@ # frozen_string_literal: true module Alchemy - # Module concerning element definitions - # - module Element::Definitions - extend ActiveSupport::Concern + class Element < BaseRecord + # Module concerning element definitions + # + module Definitions + extend ActiveSupport::Concern - module ClassMethods - # Returns the definitions from elements.yml file. - # - # Place a +elements.yml+ file inside your apps +config/alchemy+ folder to define - # your own set of elements - # - def definitions - ElementDefinition.all - end + module ClassMethods + # Returns the definitions from elements.yml file. + # + # Place a +elements.yml+ file inside your apps +config/alchemy+ folder to define + # your own set of elements + # + def definitions + ElementDefinition.all + end - # Returns one element definition by given name. - # - def definition_by_name(name) - ElementDefinition.get(name) + # Returns one element definition by given name. + # + def definition_by_name(name) + ElementDefinition.get(name) + end end - end - # The definition of this element. - # - def definition - if definition = self.class.definition_by_name(name) - definition - else - log_warning "Could not find element definition for #{name}. " \ - "Please check your elements.yml file!" - {} + # The definition of this element. + # + def definition + if definition = self.class.definition_by_name(name) + definition + else + log_warning "Could not find element definition for #{name}. " \ + "Please check your elements.yml file!" + {} + end end end end diff --git a/app/models/alchemy/element/element_contents.rb b/app/models/alchemy/element/element_contents.rb index 6175097b5a..7ba642a122 100644 --- a/app/models/alchemy/element/element_contents.rb +++ b/app/models/alchemy/element/element_contents.rb @@ -1,151 +1,153 @@ # frozen_string_literal: true -# Methods concerning contents for elements -# module Alchemy - module Element::ElementContents - # Find first content from element by given name. - def content_by_name(name) - contents_by_name(name).first - end - - # Find first content from element by given essence type. - def content_by_type(essence_type) - contents_by_type(essence_type).first - end - - # All contents from element by given name. - def contents_by_name(name) - contents.select { |content| content.name == name.to_s } - end - alias_method :all_contents_by_name, :contents_by_name + class Element < BaseRecord + # Methods concerning contents for elements + # + module ElementContents + # Find first content from element by given name. + def content_by_name(name) + contents_by_name(name).first + end - # All contents from element by given essence type. - def contents_by_type(essence_type) - contents.select do |content| - content.essence_type == Content.normalize_essence_type(essence_type) + # Find first content from element by given essence type. + def content_by_type(essence_type) + contents_by_type(essence_type).first end - end - alias_method :all_contents_by_type, :contents_by_type - # Updates all related contents by calling +update_essence+ on each of them. - # - # @param contents_attributes [Hash] - # Hash of contents attributes. - # The keys has to be the #id of the content to update. - # The values a Hash of attribute names and values - # - # @return [Boolean] - # True if +errors+ are blank or +contents_attributes+ hash is nil - # - # == Example - # - # @element.update_contents( - # "1" => {ingredient: "Title"}, - # "2" => {link: "https://google.com"} - # ) - # - def update_contents(contents_attributes) - return true if contents_attributes.nil? + # All contents from element by given name. + def contents_by_name(name) + contents.select { |content| content.name == name.to_s } + end + alias_method :all_contents_by_name, :contents_by_name - contents.each do |content| - content_hash = contents_attributes[content.id.to_s] || next - content.update_essence(content_hash) || errors.add(:base, :essence_validation_failed) + # All contents from element by given essence type. + def contents_by_type(essence_type) + contents.select do |content| + content.essence_type == Content.normalize_essence_type(essence_type) + end + end + alias_method :all_contents_by_type, :contents_by_type + + # Updates all related contents by calling +update_essence+ on each of them. + # + # @param contents_attributes [Hash] + # Hash of contents attributes. + # The keys has to be the #id of the content to update. + # The values a Hash of attribute names and values + # + # @return [Boolean] + # True if +errors+ are blank or +contents_attributes+ hash is nil + # + # == Example + # + # @element.update_contents( + # "1" => {ingredient: "Title"}, + # "2" => {link: "https://google.com"} + # ) + # + def update_contents(contents_attributes) + return true if contents_attributes.nil? + + contents.each do |content| + content_hash = contents_attributes[content.id.to_s] || next + content.update_essence(content_hash) || errors.add(:base, :essence_validation_failed) + end + errors.blank? end - errors.blank? - end - # Copy current content's contents to given target element - def copy_contents_to(element) - contents.map do |content| - Content.copy(content, element_id: element.id) + # Copy current content's contents to given target element + def copy_contents_to(element) + contents.map do |content| + Content.copy(content, element_id: element.id) + end end - end - # Returns the content that is marked as rss title. - # - # Mark a content as rss title in your +elements.yml+ file: - # - # - name: news - # contents: - # - name: headline - # type: EssenceText - # rss_title: true - # - def content_for_rss_title - content_for_rss_meta("title") - end + # Returns the content that is marked as rss title. + # + # Mark a content as rss title in your +elements.yml+ file: + # + # - name: news + # contents: + # - name: headline + # type: EssenceText + # rss_title: true + # + def content_for_rss_title + content_for_rss_meta("title") + end - # Returns the content that is marked as rss description. - # - # Mark a content as rss description in your +elements.yml+ file: - # - # - name: news - # contents: - # - name: body - # type: EssenceRichtext - # rss_description: true - # - def content_for_rss_description - content_for_rss_meta("description") - end + # Returns the content that is marked as rss description. + # + # Mark a content as rss description in your +elements.yml+ file: + # + # - name: news + # contents: + # - name: body + # type: EssenceRichtext + # rss_description: true + # + def content_for_rss_description + content_for_rss_meta("description") + end - # Returns the array with the hashes for all element contents in the elements.yml file - def content_definitions - return nil if definition.blank? + # Returns the array with the hashes for all element contents in the elements.yml file + def content_definitions + return nil if definition.blank? - definition["contents"] - end + definition["contents"] + end - # Returns the definition for given content_name - def content_definition_for(content_name) - if content_definitions.blank? - log_warning "Element #{name} is missing the content definition for #{content_name}" - nil - else - content_definitions.detect { |d| d["name"] == content_name.to_s } + # Returns the definition for given content_name + def content_definition_for(content_name) + if content_definitions.blank? + log_warning "Element #{name} is missing the content definition for #{content_name}" + nil + else + content_definitions.detect { |d| d["name"] == content_name.to_s } + end end - end - # Returns an array of all EssenceRichtext contents ids from elements - # - # This is used to re-initialize the TinyMCE editor in the element editor. - # - def richtext_contents_ids - # This is not very efficient SQL wise I know, but we need to iterate - # recursivly through all descendent elements and I don't know how to do this - # in pure SQL. Anyone with a better idea is welcome to submit a patch. - ids = contents.select(&:has_tinymce?).collect(&:id) - expanded_nested_elements = nested_elements.expanded - if expanded_nested_elements.present? - ids += expanded_nested_elements.collect(&:richtext_contents_ids) + # Returns an array of all EssenceRichtext contents ids from elements + # + # This is used to re-initialize the TinyMCE editor in the element editor. + # + def richtext_contents_ids + # This is not very efficient SQL wise I know, but we need to iterate + # recursivly through all descendent elements and I don't know how to do this + # in pure SQL. Anyone with a better idea is welcome to submit a patch. + ids = contents.select(&:has_tinymce?).collect(&:id) + expanded_nested_elements = nested_elements.expanded + if expanded_nested_elements.present? + ids += expanded_nested_elements.collect(&:richtext_contents_ids) + end + ids.flatten end - ids.flatten - end - # True, if any of the element's contents has essence validations defined. - def has_validations? - !contents.detect(&:has_validations?).blank? - end + # True, if any of the element's contents has essence validations defined. + def has_validations? + !contents.detect(&:has_validations?).blank? + end - # All element contents where the essence validation has failed. - def contents_with_errors - contents.select(&:essence_validation_failed?) - end + # All element contents where the essence validation has failed. + def contents_with_errors + contents.select(&:essence_validation_failed?) + end - private + private - def content_for_rss_meta(type) - definition = content_definitions.detect { |c| c["rss_#{type}"] } - return if definition.blank? + def content_for_rss_meta(type) + definition = content_definitions.detect { |c| c["rss_#{type}"] } + return if definition.blank? - contents.detect { |content| content.name == definition["name"] } - end + contents.detect { |content| content.name == definition["name"] } + end - # creates the contents for this element as described in the elements.yml - def create_contents - definition.fetch("contents", []).each do |attributes| - Content.create(attributes.merge(element: self)) + # creates the contents for this element as described in the elements.yml + def create_contents + definition.fetch("contents", []).each do |attributes| + Content.create(attributes.merge(element: self)) + end end end end diff --git a/app/models/alchemy/element/element_essences.rb b/app/models/alchemy/element/element_essences.rb index e94237a8dc..0a0495fbbc 100644 --- a/app/models/alchemy/element/element_essences.rb +++ b/app/models/alchemy/element/element_essences.rb @@ -1,112 +1,114 @@ # frozen_string_literal: true module Alchemy - module Element::ElementEssences - # Returns the contents essence value (aka. ingredient) for passed content name. - def ingredient(name) - content = content_by_name(name) - return nil if content.blank? + class Element < BaseRecord + module ElementEssences + # Returns the contents essence value (aka. ingredient) for passed content name. + def ingredient(name) + content = content_by_name(name) + return nil if content.blank? - content.ingredient - end + content.ingredient + end - # True if the element has a content for given name, - # that has an essence value (aka. ingredient) that is not blank. - def has_ingredient?(name) - ingredient(name).present? - end + # True if the element has a content for given name, + # that has an essence value (aka. ingredient) that is not blank. + def has_ingredient?(name) + ingredient(name).present? + end - # Returns all essence errors in the format of: - # - # { - # content.name => [ - # error_message_for_validation_1, - # error_message_for_validation_2 - # ] - # } - # - # Get translated error messages with +Element#essence_error_messages+ - # - def essence_errors - essence_errors = {} - contents.each do |content| - if content.essence_validation_failed? - essence_errors[content.name] = content.essence.validation_errors + # Returns all essence errors in the format of: + # + # { + # content.name => [ + # error_message_for_validation_1, + # error_message_for_validation_2 + # ] + # } + # + # Get translated error messages with +Element#essence_error_messages+ + # + def essence_errors + essence_errors = {} + contents.each do |content| + if content.essence_validation_failed? + essence_errors[content.name] = content.essence.validation_errors + end end + essence_errors end - essence_errors - end - # Essence validation errors - # - # == Error messages are translated via I18n - # - # Inside your translation file add translations like: - # - # alchemy: - # content_validations: - # name_of_the_element: - # name_of_the_content: - # validation_error_type: Error Message - # - # NOTE: +validation_error_type+ has to be one of: - # - # * blank - # * taken - # * invalid - # - # === Example: - # - # de: - # alchemy: - # content_validations: - # contactform: - # email: - # invalid: 'Die Email hat nicht das richtige Format' - # - # - # == Error message translation fallbacks - # - # In order to not translate every single content for every element - # you can provide default error messages per content name: - # - # === Example - # - # en: - # alchemy: - # content_validations: - # fields: - # email: - # invalid: E-Mail has wrong format - # blank: E-Mail can't be blank - # - # And even further you can provide general field agnostic error messages: - # - # === Example - # - # en: - # alchemy: - # content_validations: - # errors: - # invalid: %{field} has wrong format - # blank: %{field} can't be blank - # - def essence_error_messages - messages = [] - essence_errors.each do |content_name, errors| - errors.each do |error| - messages << Alchemy.t( - "#{name}.#{content_name}.#{error}", - scope: "content_validations", - default: [ - "fields.#{content_name}.#{error}".to_sym, - "errors.#{error}".to_sym, - ], - field: Content.translated_label_for(content_name, name), - ) + # Essence validation errors + # + # == Error messages are translated via I18n + # + # Inside your translation file add translations like: + # + # alchemy: + # content_validations: + # name_of_the_element: + # name_of_the_content: + # validation_error_type: Error Message + # + # NOTE: +validation_error_type+ has to be one of: + # + # * blank + # * taken + # * invalid + # + # === Example: + # + # de: + # alchemy: + # content_validations: + # contactform: + # email: + # invalid: 'Die Email hat nicht das richtige Format' + # + # + # == Error message translation fallbacks + # + # In order to not translate every single content for every element + # you can provide default error messages per content name: + # + # === Example + # + # en: + # alchemy: + # content_validations: + # fields: + # email: + # invalid: E-Mail has wrong format + # blank: E-Mail can't be blank + # + # And even further you can provide general field agnostic error messages: + # + # === Example + # + # en: + # alchemy: + # content_validations: + # errors: + # invalid: %{field} has wrong format + # blank: %{field} can't be blank + # + def essence_error_messages + messages = [] + essence_errors.each do |content_name, errors| + errors.each do |error| + messages << Alchemy.t( + "#{name}.#{content_name}.#{error}", + scope: "content_validations", + default: [ + "fields.#{content_name}.#{error}".to_sym, + "errors.#{error}".to_sym, + ], + field: Content.translated_label_for(content_name, name), + ) + end end + messages end - messages end end end diff --git a/app/models/alchemy/element/presenters.rb b/app/models/alchemy/element/presenters.rb index 2feb754185..75463fd5f4 100644 --- a/app/models/alchemy/element/presenters.rb +++ b/app/models/alchemy/element/presenters.rb @@ -1,108 +1,110 @@ # frozen_string_literal: true module Alchemy - # Methods used for presenting an Alchemy Element. - # - module Element::Presenters - extend ActiveSupport::Concern + class Element < BaseRecord + # Methods used for presenting an Alchemy Element. + # + module Presenters + extend ActiveSupport::Concern + + module ClassMethods + # Human name for displaying elements in select boxes and element editor views. + # + # The name is beeing translated from given name value as described in +config/alchemy/elements.yml+ + # + # Translate the name in your +config/locales+ language file. + # + # == Example: + # + # de: + # alchemy: + # element_names: + # contactform: 'Kontakt Formular' + # + # If no translation is found a humanized name is used. + # + def display_name_for(name) + Alchemy.t(name, scope: "element_names", default: name.to_s.humanize) + end + end - module ClassMethods - # Human name for displaying elements in select boxes and element editor views. + # Returns the translated name # - # The name is beeing translated from given name value as described in +config/alchemy/elements.yml+ + # @see Alchemy::Element::Presenters#display_name_for # - # Translate the name in your +config/locales+ language file. + def display_name + self.class.display_name_for(definition["name"] || name) + end + + # Returns a preview text for element. # - # == Example: + # It's taken from the first Content found in the +elements.yml+ definition file. # - # de: - # alchemy: - # element_names: - # contactform: 'Kontakt Formular' + # You can flag a Content as +as_element_title+ to take this as preview. # - # If no translation is found a humanized name is used. + # @param maxlength [Fixnum] (60) + # Length of characters after the text will be cut off. # - def display_name_for(name) - Alchemy.t(name, scope: "element_names", default: name.to_s.humanize) + def preview_text(maxlength = 60) + preview_text_from_preview_content(maxlength) || preview_text_from_nested_elements(maxlength) end - end - # Returns the translated name - # - # @see Alchemy::Element::Presenters#display_name_for - # - def display_name - self.class.display_name_for(definition["name"] || name) - end - - # Returns a preview text for element. - # - # It's taken from the first Content found in the +elements.yml+ definition file. - # - # You can flag a Content as +as_element_title+ to take this as preview. - # - # @param maxlength [Fixnum] (60) - # Length of characters after the text will be cut off. - # - def preview_text(maxlength = 60) - preview_text_from_preview_content(maxlength) || preview_text_from_nested_elements(maxlength) - end - - # Generates a preview text containing Element#display_name and Element#preview_text. - # - # It is displayed inside the head of the Element in the Elements.list overlay window from the Alchemy Admin::Page#edit view. - # - # === Example - # - # A Element described as: - # - # - name: funky_element - # display_name: Funky Element - # contents: - # - name: headline - # type: EssenceText - # - name: text - # type EssenceRichtext - # as_element_title: true - # - # With "I want to tell you a funky story" as stripped_body for the EssenceRichtext Content produces: - # - # Funky Element: I want to tell ... - # - # @param maxlength [Fixnum] (30) - # Length of characters after the text will be cut off. - # - def display_name_with_preview_text(maxlength = 30) - "#{display_name}: #{preview_text(maxlength)}" - end + # Generates a preview text containing Element#display_name and Element#preview_text. + # + # It is displayed inside the head of the Element in the Elements.list overlay window from the Alchemy Admin::Page#edit view. + # + # === Example + # + # A Element described as: + # + # - name: funky_element + # display_name: Funky Element + # contents: + # - name: headline + # type: EssenceText + # - name: text + # type EssenceRichtext + # as_element_title: true + # + # With "I want to tell you a funky story" as stripped_body for the EssenceRichtext Content produces: + # + # Funky Element: I want to tell ... + # + # @param maxlength [Fixnum] (30) + # Length of characters after the text will be cut off. + # + def display_name_with_preview_text(maxlength = 30) + "#{display_name}: #{preview_text(maxlength)}" + end - # Returns a dom id used for elements html id tag. - # - def dom_id - "#{name}_#{id}" - end + # Returns a dom id used for elements html id tag. + # + def dom_id + "#{name}_#{id}" + end - # The content that's used for element's preview text. - # - # It tries to find one of element's contents that is defined +as_element_title+. - # Takes element's first content if no content is defined +as_element_title+. - # - # @return (Alchemy::Content) - # - def preview_content - @_preview_content ||= contents.detect(&:preview_content?) || contents.first - end + # The content that's used for element's preview text. + # + # It tries to find one of element's contents that is defined +as_element_title+. + # Takes element's first content if no content is defined +as_element_title+. + # + # @return (Alchemy::Content) + # + def preview_content + @_preview_content ||= contents.detect(&:preview_content?) || contents.first + end - private + private - def preview_text_from_nested_elements(maxlength) - return if all_nested_elements.empty? + def preview_text_from_nested_elements(maxlength) + return if all_nested_elements.empty? - all_nested_elements.first.preview_text(maxlength) - end + all_nested_elements.first.preview_text(maxlength) + end - def preview_text_from_preview_content(maxlength) - preview_content.try!(:preview_text, maxlength) + def preview_text_from_preview_content(maxlength) + preview_content.try!(:preview_text, maxlength) + end end end end diff --git a/app/models/alchemy/page.rb b/app/models/alchemy/page.rb index b3209a6a2c..0111713c24 100644 --- a/app/models/alchemy/page.rb +++ b/app/models/alchemy/page.rb @@ -35,6 +35,12 @@ # locked_at :datetime # +require_dependency "alchemy/page/fixed_attributes" +require_dependency "alchemy/page/page_scopes" +require_dependency "alchemy/page/page_natures" +require_dependency "alchemy/page/page_naming" +require_dependency "alchemy/page/page_elements" + module Alchemy class Page < BaseRecord include Alchemy::Hints @@ -138,10 +144,10 @@ class Page < BaseRecord after_update -> { nodes.update_all(updated_at: Time.current) } # Concerns - include Alchemy::Page::PageScopes - include Alchemy::Page::PageNatures - include Alchemy::Page::PageNaming - include Alchemy::Page::PageElements + include PageScopes + include PageNatures + include PageNaming + include PageElements # site_name accessor delegate :name, to: :site, prefix: true, allow_nil: true @@ -460,7 +466,7 @@ def update_node!(node) # Holds an instance of +FixedAttributes+ def fixed_attributes - @_fixed_attributes ||= Alchemy::Page::FixedAttributes.new(self) + @_fixed_attributes ||= FixedAttributes.new(self) end # True if given attribute name is defined as fixed diff --git a/app/models/alchemy/page/fixed_attributes.rb b/app/models/alchemy/page/fixed_attributes.rb index 679a058eb0..a0a8ccea63 100644 --- a/app/models/alchemy/page/fixed_attributes.rb +++ b/app/models/alchemy/page/fixed_attributes.rb @@ -1,66 +1,68 @@ # frozen_string_literal: true module Alchemy - # = Fixed page attributes - # - # Fixed page attributes are not allowed to be changed by the user. - # - # Define fixed page attributes on the page layout definition of a page. - # - # == Example - # - # # page_layout.yml - # - name: Index - # unique: true - # fixed_attributes: - # - public_on: nil - # - public_until: nil - # - class Page::FixedAttributes - attr_reader :page - - def initialize(page) - @page = page - end - - # All fixed attributes defined on page + class Page < BaseRecord + # = Fixed page attributes # - # Aliased as +#all+ + # Fixed page attributes are not allowed to be changed by the user. # - # @return Hash + # Define fixed page attributes on the page layout definition of a page. # - def attributes - @_attributes ||= page.definition.fetch("fixed_attributes", {}).symbolize_keys - end - alias_method :all, :attributes - - # True if fixed attributes are defined on page - # - # Aliased as +#present?+ + # == Example # - # @return Boolean + # # page_layout.yml + # - name: Index + # unique: true + # fixed_attributes: + # - public_on: nil + # - public_until: nil # - def any? - attributes.present? - end - alias_method :present?, :any? + class FixedAttributes + attr_reader :page - # True if given attribute name is defined on page - # - # @return Boolean - # - def fixed?(name) - return false if name.nil? + def initialize(page) + @page = page + end - attributes.key?(name.to_sym) - end + # All fixed attributes defined on page + # + # Aliased as +#all+ + # + # @return Hash + # + def attributes + @_attributes ||= page.definition.fetch("fixed_attributes", {}).symbolize_keys + end + alias_method :all, :attributes - # Returns the attribute by key - # - def [](name) - return nil if name.nil? + # True if fixed attributes are defined on page + # + # Aliased as +#present?+ + # + # @return Boolean + # + def any? + attributes.present? + end + alias_method :present?, :any? + + # True if given attribute name is defined on page + # + # @return Boolean + # + def fixed?(name) + return false if name.nil? + + attributes.key?(name.to_sym) + end + + # Returns the attribute by key + # + def [](name) + return nil if name.nil? - attributes[name.to_sym] + attributes[name.to_sym] + end end end end diff --git a/app/models/alchemy/page/page_elements.rb b/app/models/alchemy/page/page_elements.rb index d53d43f50c..596a7d73e8 100644 --- a/app/models/alchemy/page/page_elements.rb +++ b/app/models/alchemy/page/page_elements.rb @@ -1,207 +1,209 @@ # frozen_string_literal: true module Alchemy - module Page::PageElements - extend ActiveSupport::Concern - - included do - attr_accessor :autogenerate_elements - - has_many :all_elements, - -> { order(:position) }, - class_name: "Alchemy::Element", - inverse_of: :page - has_many :elements, - -> { order(:position).not_nested.unfixed.available }, - class_name: "Alchemy::Element", - inverse_of: :page - has_many :fixed_elements, - -> { order(:position).fixed.available }, - class_name: "Alchemy::Element", - inverse_of: :page - has_many :dependent_destroyable_elements, - -> { not_nested }, - class_name: "Alchemy::Element", - dependent: :destroy - has_many :contents, through: :elements - has_and_belongs_to_many :to_be_swept_elements, -> { distinct }, - class_name: "Alchemy::Element", - join_table: ElementToPage.table_name - - after_create :generate_elements, - unless: -> { autogenerate_elements == false } - end + class Page < BaseRecord + module PageElements + extend ActiveSupport::Concern + + included do + attr_accessor :autogenerate_elements + + has_many :all_elements, + -> { order(:position) }, + class_name: "Alchemy::Element", + inverse_of: :page + has_many :elements, + -> { order(:position).not_nested.unfixed.available }, + class_name: "Alchemy::Element", + inverse_of: :page + has_many :fixed_elements, + -> { order(:position).fixed.available }, + class_name: "Alchemy::Element", + inverse_of: :page + has_many :dependent_destroyable_elements, + -> { not_nested }, + class_name: "Alchemy::Element", + dependent: :destroy + has_many :contents, through: :elements + has_and_belongs_to_many :to_be_swept_elements, -> { distinct }, + class_name: "Alchemy::Element", + join_table: ElementToPage.table_name + + after_create :generate_elements, + unless: -> { autogenerate_elements == false } + end - module ClassMethods - # Copy page elements - # - # @param source [Alchemy::Page] - # @param target [Alchemy::Page] - # @return [Array] - # - def copy_elements(source, target) - source_elements = source.all_elements.not_nested - source_elements.order(:position).map do |source_element| - Element.copy(source_element, { - page_id: target.id, - }).tap(&:move_to_bottom) + module ClassMethods + # Copy page elements + # + # @param source [Alchemy::Page] + # @param target [Alchemy::Page] + # @return [Array] + # + def copy_elements(source, target) + source_elements = source.all_elements.not_nested + source_elements.order(:position).map do |source_element| + Element.copy(source_element, { + page_id: target.id, + }).tap(&:move_to_bottom) + end end end - end - # All available element definitions that can actually be placed on current page. - # - # It extracts all definitions that are unique or limited and already on page. - # - # == Example of unique element: - # - # - name: headline - # unique: true - # contents: - # - name: headline - # type: EssenceText - # - # == Example of limited element: - # - # - name: article - # amount: 2 - # contents: - # - name: text - # type: EssenceRichtext - # - def available_element_definitions(only_element_named = nil) - @_element_definitions ||= if only_element_named - definition = Element.definition_by_name(only_element_named) - element_definitions_by_name(definition["nestable_elements"]) - else - element_definitions - end + # All available element definitions that can actually be placed on current page. + # + # It extracts all definitions that are unique or limited and already on page. + # + # == Example of unique element: + # + # - name: headline + # unique: true + # contents: + # - name: headline + # type: EssenceText + # + # == Example of limited element: + # + # - name: article + # amount: 2 + # contents: + # - name: text + # type: EssenceRichtext + # + def available_element_definitions(only_element_named = nil) + @_element_definitions ||= if only_element_named + definition = Element.definition_by_name(only_element_named) + element_definitions_by_name(definition["nestable_elements"]) + else + element_definitions + end - return [] if @_element_definitions.blank? + return [] if @_element_definitions.blank? - existing_elements = all_elements.not_nested - @_existing_element_names = existing_elements.pluck(:name) - delete_unique_element_definitions! - delete_outnumbered_element_definitions! + existing_elements = all_elements.not_nested + @_existing_element_names = existing_elements.pluck(:name) + delete_unique_element_definitions! + delete_outnumbered_element_definitions! - @_element_definitions - end + @_element_definitions + end - # All names of elements that can actually be placed on current page. - # - def available_element_names - @_available_element_names ||= available_element_definitions.map { |e| e["name"] } - end + # All names of elements that can actually be placed on current page. + # + def available_element_names + @_available_element_names ||= available_element_definitions.map { |e| e["name"] } + end - # Available element definitions excluding nested unique elements. - # - def available_elements_within_current_scope(parent) - @_available_elements = if parent - parents_unique_nested_elements = parent.nested_elements.where(unique: true).pluck(:name) - available_element_definitions(parent.name).reject do |e| - parents_unique_nested_elements.include? e["name"] + # Available element definitions excluding nested unique elements. + # + def available_elements_within_current_scope(parent) + @_available_elements = if parent + parents_unique_nested_elements = parent.nested_elements.where(unique: true).pluck(:name) + available_element_definitions(parent.name).reject do |e| + parents_unique_nested_elements.include? e["name"] + end + else + available_element_definitions end - else - available_element_definitions - end - end + end - # All element definitions defined for page's page layout - # - # Warning: Since elements can be unique or limited in number, - # it is more safe to ask for +available_element_definitions+ - # - def element_definitions - @_element_definitions ||= element_definitions_by_name(element_definition_names) - end + # All element definitions defined for page's page layout + # + # Warning: Since elements can be unique or limited in number, + # it is more safe to ask for +available_element_definitions+ + # + def element_definitions + @_element_definitions ||= element_definitions_by_name(element_definition_names) + end - # All element definitions defined for page's page layout including nestable element definitions - # - def descendent_element_definitions - definitions = element_definitions_by_name(element_definition_names) - definitions.select { |d| d.key?("nestable_elements") }.each do |d| - definitions += element_definitions_by_name(d["nestable_elements"]) + # All element definitions defined for page's page layout including nestable element definitions + # + def descendent_element_definitions + definitions = element_definitions_by_name(element_definition_names) + definitions.select { |d| d.key?("nestable_elements") }.each do |d| + definitions += element_definitions_by_name(d["nestable_elements"]) + end + definitions.uniq { |d| d["name"] } end - definitions.uniq { |d| d["name"] } - end - # All names of elements that are defined in the page definition. - # - # Assign elements to a page in +config/alchemy/page_layouts.yml+. - # - # == Example of page_layouts.yml: - # - # - name: contact - # elements: [headline, contactform] - # - def element_definition_names - definition["elements"] || [] - end + # All names of elements that are defined in the page definition. + # + # Assign elements to a page in +config/alchemy/page_layouts.yml+. + # + # == Example of page_layouts.yml: + # + # - name: contact + # elements: [headline, contactform] + # + def element_definition_names + definition["elements"] || [] + end + + # Element definitions with given name(s) + # + # @param [Array || String] + # one or many Alchemy::Element names. Pass +'all'+ to get all Element definitions + # @return [Array] + # An Array of element definitions + # + def element_definitions_by_name(names) + return [] if names.blank? - # Element definitions with given name(s) - # - # @param [Array || String] - # one or many Alchemy::Element names. Pass +'all'+ to get all Element definitions - # @return [Array] - # An Array of element definitions - # - def element_definitions_by_name(names) - return [] if names.blank? - - if names.to_s == "all" - Element.definitions - else - Element.definitions.select { |e| names.include? e["name"] } + if names.to_s == "all" + Element.definitions + else + Element.definitions.select { |e| names.include? e["name"] } + end end - end - # Returns all elements that should be feeded via rss. - # - # Define feedable elements in your +page_layouts.yml+: - # - # - name: news - # feed: true - # feed_elements: [element_name, element_2_name] - # - def feed_elements - elements.named(definition["feed_elements"]) - end + # Returns all elements that should be feeded via rss. + # + # Define feedable elements in your +page_layouts.yml+: + # + # - name: news + # feed: true + # feed_elements: [element_name, element_2_name] + # + def feed_elements + elements.named(definition["feed_elements"]) + end - # Returns an array of all EssenceRichtext contents ids from not folded elements - # - def richtext_contents_ids - Alchemy::Content.joins(:element) - .where(Element.table_name => { page_id: id, folded: false }) - .select(&:has_tinymce?) - .collect(&:id) - end + # Returns an array of all EssenceRichtext contents ids from not folded elements + # + def richtext_contents_ids + Alchemy::Content.joins(:element) + .where(Element.table_name => { page_id: id, folded: false }) + .select(&:has_tinymce?) + .collect(&:id) + end - private + private - # Looks in the page_layout descripion, if there are elements to autogenerate. - # - # And if so, it generates them. - # - def generate_elements - definition.fetch("autogenerate", []).each do |element_name| - Element.create(page: self, name: element_name) + # Looks in the page_layout descripion, if there are elements to autogenerate. + # + # And if so, it generates them. + # + def generate_elements + definition.fetch("autogenerate", []).each do |element_name| + Element.create(page: self, name: element_name) + end end - end - # Deletes unique and already present definitions from @_element_definitions. - # - def delete_unique_element_definitions! - @_element_definitions.delete_if do |element| - element["unique"] && @_existing_element_names.include?(element["name"]) + # Deletes unique and already present definitions from @_element_definitions. + # + def delete_unique_element_definitions! + @_element_definitions.delete_if do |element| + element["unique"] && @_existing_element_names.include?(element["name"]) + end end - end - # Deletes limited and outnumbered definitions from @_element_definitions. - # - def delete_outnumbered_element_definitions! - @_element_definitions.delete_if do |element| - outnumbered = @_existing_element_names.select { |name| name == element["name"] } - element["amount"] && outnumbered.count >= element["amount"].to_i + # Deletes limited and outnumbered definitions from @_element_definitions. + # + def delete_outnumbered_element_definitions! + @_element_definitions.delete_if do |element| + outnumbered = @_existing_element_names.select { |name| name == element["name"] } + element["amount"] && outnumbered.count >= element["amount"].to_i + end end end end diff --git a/app/models/alchemy/page/page_naming.rb b/app/models/alchemy/page/page_naming.rb index 6c6f8f61b5..af84478200 100644 --- a/app/models/alchemy/page/page_naming.rb +++ b/app/models/alchemy/page/page_naming.rb @@ -1,85 +1,87 @@ # frozen_string_literal: true module Alchemy - module Page::PageNaming - extend ActiveSupport::Concern - include NameConversions - RESERVED_URLNAMES = %w(admin messages new) + class Page < BaseRecord + module PageNaming + extend ActiveSupport::Concern + include NameConversions + RESERVED_URLNAMES = %w(admin messages new) - included do - before_validation :set_urlname, - if: :renamed?, - unless: -> { name.blank? } + included do + before_validation :set_urlname, + if: :renamed?, + unless: -> { name.blank? } - validates :name, - presence: true - validates :urlname, - uniqueness: { scope: [:language_id, :layoutpage], if: -> { urlname.present? } }, - exclusion: { in: RESERVED_URLNAMES }, - length: { minimum: 3, if: -> { urlname.present? } } + validates :name, + presence: true + validates :urlname, + uniqueness: { scope: [:language_id, :layoutpage], if: -> { urlname.present? } }, + exclusion: { in: RESERVED_URLNAMES }, + length: { minimum: 3, if: -> { urlname.present? } } - before_save :set_title, - if: -> { title.blank? } + before_save :set_title, + if: -> { title.blank? } - after_update :update_descendants_urlnames, - if: :saved_change_to_urlname? + after_update :update_descendants_urlnames, + if: :saved_change_to_urlname? - after_move :update_urlname! - end + after_move :update_urlname! + end - # Returns true if name or urlname has changed. - def renamed? - name_changed? || urlname_changed? - end + # Returns true if name or urlname has changed. + def renamed? + name_changed? || urlname_changed? + end - # Makes a slug of all ancestors urlnames including mine and delimit them be slash. - # So the whole path is stored as urlname in the database. - def update_urlname! - new_urlname = nested_url_name - if urlname != new_urlname - legacy_urls.create(urlname: urlname) - update_column(:urlname, new_urlname) + # Makes a slug of all ancestors urlnames including mine and delimit them be slash. + # So the whole path is stored as urlname in the database. + def update_urlname! + new_urlname = nested_url_name + if urlname != new_urlname + legacy_urls.create(urlname: urlname) + update_column(:urlname, new_urlname) + end end - end - # Returns always the last part of a urlname path - def slug - urlname.to_s.split("/").last - end + # Returns always the last part of a urlname path + def slug + urlname.to_s.split("/").last + end - private + private - def update_descendants_urlnames - reload - descendants.each(&:update_urlname!) - end + def update_descendants_urlnames + reload + descendants.each(&:update_urlname!) + end - # Sets the urlname to a url friendly slug. - # Either from name, or if present, from urlname. - # The urlname contains the whole path including parent urlnames. - def set_urlname - self[:urlname] = nested_url_name - end + # Sets the urlname to a url friendly slug. + # Either from name, or if present, from urlname. + # The urlname contains the whole path including parent urlnames. + def set_urlname + self[:urlname] = nested_url_name + end - def set_title - self[:title] = name - end + def set_title + self[:title] = name + end - # Converts the given name into an url friendly string. - # - # Names shorter than 3 will be filled up with dashes, - # so it does not collidate with the language code. - # - def converted_url_name - url_name = convert_to_urlname(slug.blank? ? name : slug) - url_name.rjust(3, "-") - end + # Converts the given name into an url friendly string. + # + # Names shorter than 3 will be filled up with dashes, + # so it does not collidate with the language code. + # + def converted_url_name + url_name = convert_to_urlname(slug.blank? ? name : slug) + url_name.rjust(3, "-") + end - def nested_url_name - if parent&.language_root? - converted_url_name - else - [parent&.urlname, converted_url_name].compact.join("/") + def nested_url_name + if parent&.language_root? + converted_url_name + else + [parent&.urlname, converted_url_name].compact.join("/") + end end end end diff --git a/app/models/alchemy/page/page_natures.rb b/app/models/alchemy/page/page_natures.rb index 7242d8a812..0f53480f48 100644 --- a/app/models/alchemy/page/page_natures.rb +++ b/app/models/alchemy/page/page_natures.rb @@ -1,171 +1,173 @@ # frozen_string_literal: true module Alchemy - module Page::PageNatures - extend ActiveSupport::Concern + class Page < BaseRecord + module PageNatures + extend ActiveSupport::Concern - def public? - current_time = Time.current - language.public? && already_public_for?(current_time) && still_public_for?(current_time) - end + def public? + current_time = Time.current + language.public? && already_public_for?(current_time) && still_public_for?(current_time) + end - def expiration_time - public_until? ? public_until - Time.current : nil - end + def expiration_time + public_until? ? public_until - Time.current : nil + end - def taggable? - definition["taggable"] == true - end + def taggable? + definition["taggable"] == true + end - deprecate :taggable?, deprecator: Alchemy::Deprecation + deprecate :taggable?, deprecator: Alchemy::Deprecation - def rootpage? - !new_record? && parent_id.blank? - end + def rootpage? + !new_record? && parent_id.blank? + end - def folded?(user_id) - return unless Alchemy.user_class < ActiveRecord::Base + def folded?(user_id) + return unless Alchemy.user_class < ActiveRecord::Base - folded_pages.where(user_id: user_id, folded: true).any? - end + folded_pages.where(user_id: user_id, folded: true).any? + end - def contains_feed? - definition["feed"] - end + def contains_feed? + definition["feed"] + end - # Returns an Array of Alchemy roles which are able to edit this template - # - # # config/alchemy/page_layouts.yml - # - name: contact - # editable_by: - # - freelancer - # - admin - # - # @returns Array - # - def has_limited_editors? - definition["editable_by"].present? - end + # Returns an Array of Alchemy roles which are able to edit this template + # + # # config/alchemy/page_layouts.yml + # - name: contact + # editable_by: + # - freelancer + # - admin + # + # @returns Array + # + def has_limited_editors? + definition["editable_by"].present? + end - def editor_roles - return unless has_limited_editors? + def editor_roles + return unless has_limited_editors? - definition["editable_by"] - end + definition["editable_by"] + end - # True if page locked_at timestamp and locked_by id are set - def locked? - locked_by? && locked_at? - end + # True if page locked_at timestamp and locked_by id are set + def locked? + locked_by? && locked_at? + end - # Returns a Hash describing the status of the Page. - # - def status - { - public: public?, - locked: locked?, - restricted: restricted?, - } - end + # Returns a Hash describing the status of the Page. + # + def status + { + public: public?, + locked: locked?, + restricted: restricted?, + } + end - # Returns the translated status for given status type. - # - # @param [Symbol] status_type - # - def status_title(status_type) - Alchemy.t(status[status_type].to_s, scope: "page_states.#{status_type}") - end + # Returns the translated status for given status type. + # + # @param [Symbol] status_type + # + def status_title(status_type) + Alchemy.t(status[status_type].to_s, scope: "page_states.#{status_type}") + end - # Returns the self#page_layout definition from config/alchemy/page_layouts.yml file. - def definition - definition = PageLayout.get(page_layout) - if definition.nil? - log_warning "Page definition for `#{page_layout}` not found. Please check `page_layouts.yml` file." - return {} + # Returns the self#page_layout definition from config/alchemy/page_layouts.yml file. + def definition + definition = PageLayout.get(page_layout) + if definition.nil? + log_warning "Page definition for `#{page_layout}` not found. Please check `page_layouts.yml` file." + return {} + end + definition end - definition - end - # Returns translated name of the pages page_layout value. - # Page layout names are defined inside the config/alchemy/page_layouts.yml file. - # Translate the name in your config/locales language yml file. - def layout_display_name - Alchemy.t(page_layout, scope: "page_layout_names") - end + # Returns translated name of the pages page_layout value. + # Page layout names are defined inside the config/alchemy/page_layouts.yml file. + # Translate the name in your config/locales language yml file. + def layout_display_name + Alchemy.t(page_layout, scope: "page_layout_names") + end - # Returns the name for the layout partial - # - def layout_partial_name - page_layout.parameterize.underscore - end + # Returns the name for the layout partial + # + def layout_partial_name + page_layout.parameterize.underscore + end - # Returns the key that's taken for cache path. - # - # Uses the +published_at+ value that's updated when the user publishes the page. - # - # If the page is the current preview it uses the updated_at value as cache key. - # - def cache_key - if Page.current_preview == self - "alchemy/pages/#{id}-#{updated_at}" - else - "alchemy/pages/#{id}-#{published_at}" + # Returns the key that's taken for cache path. + # + # Uses the +published_at+ value that's updated when the user publishes the page. + # + # If the page is the current preview it uses the updated_at value as cache key. + # + def cache_key + if Page.current_preview == self + "alchemy/pages/#{id}-#{updated_at}" + else + "alchemy/pages/#{id}-#{published_at}" + end end - end - # We use the published_at value for the cache_key. - # - # If no published_at value is set yet, i.e. because it was never published, - # we return the updated_at value. - # - def published_at - read_attribute(:published_at) || updated_at - end + # We use the published_at value for the cache_key. + # + # If no published_at value is set yet, i.e. because it was never published, + # we return the updated_at value. + # + def published_at + read_attribute(:published_at) || updated_at + end - # Returns true if the page cache control headers should be set. - # - # == Disable Alchemy's page caching globally - # - # # config/alchemy/config.yml - # ... - # cache_pages: false - # - # == Disable caching on page layout level - # - # # config/alchemy/page_layouts.yml - # - name: contact - # cache: false - # - # == Note: - # - # This only sets the cache control headers and skips rendering of the page body, - # if the cache is fresh. - # This does not disable the fragment caching in the views. - # So if you don't want a page and it's elements to be cached, - # then be sure to not use <% cache element %> in the views. - # - # @returns Boolean - # - def cache_page? - return false unless caching_enabled? - - page_layout = PageLayout.get(self.page_layout) - page_layout["cache"] != false && page_layout["searchresults"] != true - end + # Returns true if the page cache control headers should be set. + # + # == Disable Alchemy's page caching globally + # + # # config/alchemy/config.yml + # ... + # cache_pages: false + # + # == Disable caching on page layout level + # + # # config/alchemy/page_layouts.yml + # - name: contact + # cache: false + # + # == Note: + # + # This only sets the cache control headers and skips rendering of the page body, + # if the cache is fresh. + # This does not disable the fragment caching in the views. + # So if you don't want a page and it's elements to be cached, + # then be sure to not use <% cache element %> in the views. + # + # @returns Boolean + # + def cache_page? + return false unless caching_enabled? + + page_layout = PageLayout.get(self.page_layout) + page_layout["cache"] != false && page_layout["searchresults"] != true + end - private + private - def caching_enabled? - Alchemy::Config.get(:cache_pages) && - Rails.application.config.action_controller.perform_caching - end + def caching_enabled? + Alchemy::Config.get(:cache_pages) && + Rails.application.config.action_controller.perform_caching + end - def already_public_for?(time) - !public_on.nil? && public_on <= time - end + def already_public_for?(time) + !public_on.nil? && public_on <= time + end - def still_public_for?(time) - public_until.nil? || public_until >= time + def still_public_for?(time) + public_until.nil? || public_until >= time + end end end end diff --git a/app/models/alchemy/page/page_scopes.rb b/app/models/alchemy/page/page_scopes.rb index 0b55cca544..1dacfbf597 100644 --- a/app/models/alchemy/page/page_scopes.rb +++ b/app/models/alchemy/page/page_scopes.rb @@ -3,111 +3,113 @@ module Alchemy # ActiveRecord scopes for Alchemy::Page # - module Page::PageScopes - extend ActiveSupport::Concern - - included do - # All language root pages - # - scope :language_roots, -> { where(language_root: true) } - - # All layout pages - # - scope :layoutpages, -> { where(layoutpage: true) } - - # All locked pages - # - scope :locked, -> { where.not(locked_at: nil).where.not(locked_by: nil) } - - # All pages locked by given user - # - scope :locked_by, - ->(user) { - if user.class.respond_to? :primary_key - locked.where(locked_by: user.send(user.class.primary_key)) - end - } - - # All not locked pages - # - scope :not_locked, -> { where(locked_at: nil, locked_by: nil) } - - # All not restricted pages - # - scope :not_restricted, -> { where(restricted: false) } - - # All restricted pages - # - scope :restricted, -> { where(restricted: true) } - - # All pages that are a published language root - # - scope :public_language_roots, - -> { - published.language_roots.where( - language_code: Language.published.pluck(:language_code), - ) - } - - # Last 5 pages that where recently edited by given user - # - scope :all_last_edited_from, - ->(user) { - where(updater_id: user.id).order("updated_at DESC").limit(5) - } - - # Returns all pages that have the given +language_id+ - # - scope :with_language, ->(language_id) { where(language_id: language_id) } - - # Returns all content pages. - # - scope :contentpages, -> { where(layoutpage: [false, nil]) } - - # Returns all public contentpages that are not locked. - # - # Used for flushing all pages caches at once. - # - scope :flushables, -> { not_locked.published.contentpages } - - # Returns all layoutpages that are not locked. - # - # Used for flushing all pages caches at once. - # - scope :flushable_layoutpages, -> { not_locked.layoutpages } - - # All searchable pages - # - scope :searchables, -> { not_restricted.published.contentpages } - - # All pages from +Alchemy::Site.current+ - # - scope :from_current_site, - -> { - where(Language.table_name => { site_id: Site.current || Site.default }).joins(:language) - } - - # All pages for xml sitemap - # - scope :sitemap, -> { from_current_site.published.contentpages.where(sitemap: true) } - end - - module ClassMethods - # All public pages - # - def published - joins(:language).merge(Language.published). - where("#{table_name}.public_on <= :time AND " \ - "(#{table_name}.public_until IS NULL " \ - "OR #{table_name}.public_until >= :time)", time: Time.current) + class Page < BaseRecord + module PageScopes + extend ActiveSupport::Concern + + included do + # All language root pages + # + scope :language_roots, -> { where(language_root: true) } + + # All layout pages + # + scope :layoutpages, -> { where(layoutpage: true) } + + # All locked pages + # + scope :locked, -> { where.not(locked_at: nil).where.not(locked_by: nil) } + + # All pages locked by given user + # + scope :locked_by, + ->(user) { + if user.class.respond_to? :primary_key + locked.where(locked_by: user.send(user.class.primary_key)) + end + } + + # All not locked pages + # + scope :not_locked, -> { where(locked_at: nil, locked_by: nil) } + + # All not restricted pages + # + scope :not_restricted, -> { where(restricted: false) } + + # All restricted pages + # + scope :restricted, -> { where(restricted: true) } + + # All pages that are a published language root + # + scope :public_language_roots, + -> { + published.language_roots.where( + language_code: Language.published.pluck(:language_code), + ) + } + + # Last 5 pages that where recently edited by given user + # + scope :all_last_edited_from, + ->(user) { + where(updater_id: user.id).order("updated_at DESC").limit(5) + } + + # Returns all pages that have the given +language_id+ + # + scope :with_language, ->(language_id) { where(language_id: language_id) } + + # Returns all content pages. + # + scope :contentpages, -> { where(layoutpage: [false, nil]) } + + # Returns all public contentpages that are not locked. + # + # Used for flushing all pages caches at once. + # + scope :flushables, -> { not_locked.published.contentpages } + + # Returns all layoutpages that are not locked. + # + # Used for flushing all pages caches at once. + # + scope :flushable_layoutpages, -> { not_locked.layoutpages } + + # All searchable pages + # + scope :searchables, -> { not_restricted.published.contentpages } + + # All pages from +Alchemy::Site.current+ + # + scope :from_current_site, + -> { + where(Language.table_name => { site_id: Site.current || Site.default }).joins(:language) + } + + # All pages for xml sitemap + # + scope :sitemap, -> { from_current_site.published.contentpages.where(sitemap: true) } end - # All not public pages - # - def not_public - where("#{table_name}.public_on IS NULL OR " \ - "#{table_name}.public_on >= :time OR " \ - "#{table_name}.public_until <= :time", time: Time.current) + module ClassMethods + # All public pages + # + def published + joins(:language).merge(Language.published). + where("#{table_name}.public_on <= :time AND " \ + "(#{table_name}.public_until IS NULL " \ + "OR #{table_name}.public_until >= :time)", time: Time.current) + end + + # All not public pages + # + def not_public + where("#{table_name}.public_on IS NULL OR " \ + "#{table_name}.public_on >= :time OR " \ + "#{table_name}.public_until <= :time", time: Time.current) + end end end end