<commit_before># frozen_string_literal: true

# == Schema Information
#
# Table name: alchemy_elements
#
#  id                :integer          not null, primary key
#  name              :string
#  position          :integer
#  page_version_id   :integer          not null
#  public            :boolean          default(TRUE)
#  fixed             :boolean          default(FALSE)
#  folded            :boolean          default(FALSE)
#  unique            :boolean          default(FALSE)
#  created_at        :datetime         not null
#  updated_at        :datetime         not null
#  creator_id        :integer
#  updater_id        :integer
#  cached_tag_list   :text
#  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/

    include Alchemy::Logger
    include Alchemy::Taggable
    include Alchemy::Hints

    FORBIDDEN_DEFINITION_ATTRIBUTES = [
      "amount",
      "autogenerate",
      "nestable_elements",
      "contents",
      "hint",
      "taggable",
      "compact",
      "message",
      "deprecated",
    ].freeze

    # All Elements that share the same page version and parent element and are fixed or not are considered a list.
    #
    # If parent_element_id is nil (typical case for a simple page),
    # then all elements on that page are still in one list,
    # because acts_as_list correctly creates this statement:
    #
    #   WHERE page_version_id = 1 and fixed = FALSE AND parent_element_id = NULL
    #
    acts_as_list scope: [:page_version_id, :fixed, :parent_element_id]

    stampable stamper_class_name: Alchemy.user_class_name

    has_many :contents, dependent: :destroy, inverse_of: :element

    before_destroy :delete_all_nested_elements

    has_many :all_nested_elements,
      -> { order(:position) },
      class_name: "Alchemy::Element",
      foreign_key: :parent_element_id,
      dependent: :destroy

    has_many :nested_elements,
      -> { order(:position).available },
      class_name: "Alchemy::Element",
      foreign_key: :parent_element_id,
      dependent: :destroy,
      inverse_of: :parent_element

    belongs_to :page_version, touch: true, inverse_of: :elements
    has_one :page, through: :page_version

    # A nested element belongs to a parent element.
    belongs_to :parent_element,
      class_name: "Alchemy::Element",
      optional: true,
      touch: true,
      inverse_of: :nested_elements

    has_and_belongs_to_many :touchable_pages, -> { distinct },
      class_name: "Alchemy::Page",
      join_table: ElementToPage.table_name

    validates_presence_of :name, on: :create
    validates_format_of :name, on: :create, with: NAME_REGEXP

    attr_accessor :autogenerate_contents
    attr_accessor :autogenerate_nested_elements
    after_create :create_contents, unless: -> { autogenerate_contents == false }
    after_create :generate_nested_elements, unless: -> { autogenerate_nested_elements == false }

    after_update :touch_touchable_pages

    scope :published, -> { where(public: true) }
    scope :hidden, -> { where(public: false) }
    scope :not_restricted, -> { joins(:page).merge(Page.not_restricted) }
    scope :available, -> { published }
    scope :named, ->(names) { where(name: names) }
    scope :excluded, ->(names) { where.not(name: names) }
    scope :fixed, -> { where(fixed: true) }
    scope :unfixed, -> { where(fixed: false) }
    scope :from_current_site, -> { where(Language.table_name => { site_id: Site.current || Site.default }).joins(page: "language") }
    scope :folded, -> { where(folded: true) }
    scope :expanded, -> { where(folded: false) }
    scope :not_nested, -> { where(parent_element_id: nil) }

    delegate :restricted?, to: :page, allow_nil: true

    # Concerns
    include Definitions
    include ElementContents
    include ElementEssences
    include Presenters

    # class methods
    class << self
      # Builds a new element as described in +/config/alchemy/elements.yml+
      #
      # - Returns a new Alchemy::Element object if no name is given in attributes,
      #   because the definition can not be found w/o name
      # - Raises Alchemy::ElementDefinitionError if no definition for given attributes[:name]
      #   could be found
      #
      def new(attributes = {})
        return super if attributes[:name].blank?

        element_attributes = attributes.to_h.merge(name: attributes[:name].split("#").first)
        element_definition = Element.definition_by_name(element_attributes[:name])
        if element_definition.nil?
          raise(ElementDefinitionError, attributes)
        end

        super(element_definition.merge(element_attributes).except(*FORBIDDEN_DEFINITION_ATTRIBUTES))
      end

      # This methods does a copy of source and all depending contents and all of their depending essences.
      #
      # == Options
      #
      # You can pass a differences Hash as second option to update attributes for the copy.
      #
      # == Example
      #
      #   @copy = Alchemy::Element.copy(@element, {public: false})
      #   @copy.public? # => false
      #
      def copy(source_element, differences = {})
        DuplicateElement.new(source_element).call(differences)<commit_msg>Use fully qualified constant in Element#copy<commit_after># frozen_string_literal: true

# == Schema Information
#
# Table name: alchemy_elements
#
#  id                :integer          not null, primary key
#  name              :string
#  position          :integer
#  page_version_id   :integer          not null
#  public            :boolean          default(TRUE)
#  fixed             :boolean          default(FALSE)
#  folded            :boolean          default(FALSE)
#  unique            :boolean          default(FALSE)
#  created_at        :datetime         not null
#  updated_at        :datetime         not null
#  creator_id        :integer
#  updater_id        :integer
#  cached_tag_list   :text
#  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/

    include Alchemy::Logger
    include Alchemy::Taggable
    include Alchemy::Hints

    FORBIDDEN_DEFINITION_ATTRIBUTES = [
      "amount",
      "autogenerate",
      "nestable_elements",
      "contents",
      "hint",
      "taggable",
      "compact",
      "message",
      "deprecated",
    ].freeze

    # All Elements that share the same page version and parent element and are fixed or not are considered a list.
    #
    # If parent_element_id is nil (typical case for a simple page),
    # then all elements on that page are still in one list,
    # because acts_as_list correctly creates this statement:
    #
    #   WHERE page_version_id = 1 and fixed = FALSE AND parent_element_id = NULL
    #
    acts_as_list scope: [:page_version_id, :fixed, :parent_element_id]

    stampable stamper_class_name: Alchemy.user_class_name

    has_many :contents, dependent: :destroy, inverse_of: :element

    before_destroy :delete_all_nested_elements

    has_many :all_nested_elements,
      -> { order(:position) },
      class_name: "Alchemy::Element",
      foreign_key: :parent_element_id,
      dependent: :destroy

    has_many :nested_elements,
      -> { order(:position).available },
      class_name: "Alchemy::Element",
      foreign_key: :parent_element_id,
      dependent: :destroy,
      inverse_of: :parent_element

    belongs_to :page_version, touch: true, inverse_of: :elements
    has_one :page, through: :page_version

    # A nested element belongs to a parent element.
    belongs_to :parent_element,
      class_name: "Alchemy::Element",
      optional: true,
      touch: true,
      inverse_of: :nested_elements

    has_and_belongs_to_many :touchable_pages, -> { distinct },
      class_name: "Alchemy::Page",
      join_table: ElementToPage.table_name

    validates_presence_of :name, on: :create
    validates_format_of :name, on: :create, with: NAME_REGEXP

    attr_accessor :autogenerate_contents
    attr_accessor :autogenerate_nested_elements
    after_create :create_contents, unless: -> { autogenerate_contents == false }
    after_create :generate_nested_elements, unless: -> { autogenerate_nested_elements == false }

    after_update :touch_touchable_pages

    scope :published, -> { where(public: true) }
    scope :hidden, -> { where(public: false) }
    scope :not_restricted, -> { joins(:page).merge(Page.not_restricted) }
    scope :available, -> { published }
    scope :named, ->(names) { where(name: names) }
    scope :excluded, ->(names) { where.not(name: names) }
    scope :fixed, -> { where(fixed: true) }
    scope :unfixed, -> { where(fixed: false) }
    scope :from_current_site, -> { where(Language.table_name => { site_id: Site.current || Site.default }).joins(page: "language") }
    scope :folded, -> { where(folded: true) }
    scope :expanded, -> { where(folded: false) }
    scope :not_nested, -> { where(parent_element_id: nil) }

    delegate :restricted?, to: :page, allow_nil: true

    # Concerns
    include Definitions
    include ElementContents
    include ElementEssences
    include Presenters

    # class methods
    class << self
      # Builds a new element as described in +/config/alchemy/elements.yml+
      #
      # - Returns a new Alchemy::Element object if no name is given in attributes,
      #   because the definition can not be found w/o name
      # - Raises Alchemy::ElementDefinitionError if no definition for given attributes[:name]
      #   could be found
      #
      def new(attributes = {})
        return super if attributes[:name].blank?

        element_attributes = attributes.to_h.merge(name: attributes[:name].split("#").first)
        element_definition = Element.definition_by_name(element_attributes[:name])
        if element_definition.nil?
          raise(ElementDefinitionError, attributes)
        end

        super(element_definition.merge(element_attributes).except(*FORBIDDEN_DEFINITION_ATTRIBUTES))
      end

      # This methods does a copy of source and all depending contents and all of their depending essences.
      #
      # == Options
      #
      # You can pass a differences Hash as second option to update attributes for the copy.
      #
      # == Example
      #
      #   @copy = Alchemy::Element.copy(@element, {public: false})
      #   @copy.public? # => false
      #
      def copy(source_element, differences = {})
        Alchemy::DuplicateElement.new(source_element).call(differences)
{'fraction_non_alphanumeric': 0.09678899082568808, 'fraction_numerical': 0.0005504587155963303, 'mean_word_length': 2.9525018129079044, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 41, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5082505', 'n_tokens_mistral': 3260, 'n_tokens_neox': 3133, 'n_words': 820}