Class: Alchemy::Element

Inherits:
ActiveRecord::Base
  • Object
show all
Includes:
Definitions, Presenters, Hints, Logger, Touching
Defined in:
app/models/alchemy/element.rb

Defined Under Namespace

Modules: Definitions, Presenters

Constant Summary collapse

FORBIDDEN_DEFINITION_ATTRIBUTES =
%w(contents available_contents amount picture_gallery taggable hint)
SKIPPED_ATTRIBUTES_ON_COPY =
%w(id position folded created_at updated_at creator_id updater_id cached_tag_list)

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Presenters

#display_name, #display_name_with_preview_text, #dom_id, #preview_text

Methods included from Definitions

#definition

Methods included from Hints

#has_hint?, #hint

Methods included from Touching

#touch

Methods included from Logger

#log_warning, warn

Instance Attribute Details

#create_contents_after_createObject

Returns the value of attribute create_contents_after_create.



45
46
47
# File 'app/models/alchemy/element.rb', line 45

def create_contents_after_create
  @create_contents_after_create
end

Class Method Details

.all_from_clipboard(clipboard) ⇒ Object



124
125
126
127
# File 'app/models/alchemy/element.rb', line 124

def all_from_clipboard(clipboard)
  return [] if clipboard.nil?
  where(id: clipboard.collect { |e| e['id'] })
end

.all_from_clipboard_for_page(clipboard, page) ⇒ Object

All elements in clipboard that could be placed on page



131
132
133
134
135
136
# File 'app/models/alchemy/element.rb', line 131

def all_from_clipboard_for_page(clipboard, page)
  return [] if clipboard.nil? || page.nil?
  all_from_clipboard(clipboard).select { |ce|
    page.available_element_names.include?(ce.name)
  }
end

.copy(source, differences = {}) ⇒ Object

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


111
112
113
114
115
116
117
118
119
120
121
122
# File 'app/models/alchemy/element.rb', line 111

def copy(source, differences = {})
  source.attributes.stringify_keys!
  differences.stringify_keys!
  attributes = source.attributes.except(*SKIPPED_ATTRIBUTES_ON_COPY).merge(differences)
  element = self.create!(attributes.merge(:create_contents_after_create => false))
  element.tag_list = source.tag_list
  source.contents.each do |content|
    new_content = Content.copy(content, :element_id => element.id)
    new_content.move_to_bottom
  end
  element
end

.create_from_scratch(attributes) ⇒ Object

Creates 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 could be found



94
95
96
97
98
# File 'app/models/alchemy/element.rb', line 94

def create_from_scratch(attributes)
  element = new_from_scratch(attributes)
  element.save if element
  element
end

.new_from_scratch(attributes = {}) ⇒ Object

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 could be found



78
79
80
81
82
83
84
85
# File 'app/models/alchemy/element.rb', line 78

def new_from_scratch(attributes = {})
  attributes = attributes.dup.symbolize_keys

  return new if attributes[:name].blank?

  new_element_from_definition_by(attributes) ||
    raise(ElementDefinitionError.new(attributes))
end

Instance Method Details

#all_contents_by_name(name) ⇒ Object



198
199
200
# File 'app/models/alchemy/element.rb', line 198

def all_contents_by_name(name)
  self.contents.where(:name => name)
end

#all_contents_by_type(essence_type) ⇒ Object



202
203
204
# File 'app/models/alchemy/element.rb', line 202

def all_contents_by_type(essence_type)
  self.contents.where(:essence_type => Content.normalize_essence_type(essence_type))
end

#available_content_description_for(content_name) ⇒ Object

Returns the definition for given content_name inside the available_contents



251
252
253
254
# File 'app/models/alchemy/element.rb', line 251

def available_content_description_for(content_name)
  return nil if available_contents.blank?
  available_contents.detect { |d| d['name'] == content_name }
end

#available_contentsObject

returns the collection of available essence_types that can be created for this element depending on its description in elements.yml



257
258
259
# File 'app/models/alchemy/element.rb', line 257

def available_contents
  definition['available_contents']
end

#available_page_cell_names(page) ⇒ Object

The names of all cells from given page this element could be placed in.



410
411
412
413
414
415
416
417
# File 'app/models/alchemy/element.rb', line 410

def available_page_cell_names(page)
  cellnames = unique_available_page_cell_names(page)
  if cellnames.blank? || !page.has_cells?
    ['for_other_elements']
  else
    cellnames
  end
end

#cache_keyObject

Returns the key that’s taken for cache path.

Uses the page’s published_at value that’s updated when the user publishes the page.

If the page is the current preview it uses the element’s updated_at value as cache key.



448
449
450
451
452
453
454
# File 'app/models/alchemy/element.rb', line 448

def cache_key
  if Page.current_preview == self.page
    "alchemy/elements/#{id}-#{updated_at}"
  else
    "alchemy/elements/#{id}-#{page.published_at}"
  end
end

#content_by_name(name) ⇒ Object



190
191
192
# File 'app/models/alchemy/element.rb', line 190

def content_by_name(name)
  self.contents.find_by_name(name)
end

#content_by_type(essence_type) ⇒ Object



194
195
196
# File 'app/models/alchemy/element.rb', line 194

def content_by_type(essence_type)
  self.contents.find_by_essence_type(Content.normalize_essence_type(essence_type))
end

#content_description_for(content_name) ⇒ Object

Returns the definition for given content_name



241
242
243
244
245
246
247
248
# File 'app/models/alchemy/element.rb', line 241

def content_description_for(content_name)
  if content_descriptions.blank?
    log_warning "Element #{self.name} is missing the content definition for #{content_name}"
    return nil
  else
    content_descriptions.detect { |d| d['name'] == content_name }
  end
end

#content_descriptionsObject

Returns the array with the hashes for all element contents in the elements.yml file



235
236
237
238
# File 'app/models/alchemy/element.rb', line 235

def content_descriptions
  return nil if definition.blank?
  definition['contents']
end

#content_for_rss_descriptionObject

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


230
231
232
# File 'app/models/alchemy/element.rb', line 230

def content_for_rss_description
  content_for_rss_meta('description')
end

#content_for_rss_titleObject

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


216
217
218
# File 'app/models/alchemy/element.rb', line 216

def content_for_rss_title
  content_for_rss_meta('title')
end

#contents_with_errorsObject



389
390
391
# File 'app/models/alchemy/element.rb', line 389

def contents_with_errors
  contents.select(&:essence_validation_failed?)
end

#essence_error_messagesObject

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


371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
# File 'app/models/alchemy/element.rb', line 371

def essence_error_messages
  messages = []
  essence_errors.each do |content_name, errors|
    errors.each do |error|
      messages << I18n.t(
        "#{self.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

#essence_errorsObject

Returns all essence_errors in the format:

{
  essence.content.name => [error_message_for_validation_1, error_message_for_validation_2]
}

Get translated error messages with Element#essence_error_messages



308
309
310
311
312
313
314
315
316
# File 'app/models/alchemy/element.rb', line 308

def essence_errors
  essence_errors = {}
  essences.each do |essence|
    unless essence.errors.blank?
      essence_errors[essence.content.name] = essence.validation_errors
    end
  end
  essence_errors
end

#essencesObject



295
296
297
298
# File 'app/models/alchemy/element.rb', line 295

def essences
  return [] if contents.blank?
  contents.collect(&:essence)
end

#has_ingredient?(name) ⇒ Boolean

Returns:

  • (Boolean)


268
269
270
# File 'app/models/alchemy/element.rb', line 268

def has_ingredient?(name)
  self.ingredient(name).present?
end

#has_validations?Boolean

Returns:

  • (Boolean)


393
394
395
# File 'app/models/alchemy/element.rb', line 393

def has_validations?
  !contents.detect(&:has_validations?).blank?
end

#ingredient(name) ⇒ Object

Returns the contents ingredient for passed content name.



262
263
264
265
266
# File 'app/models/alchemy/element.rb', line 262

def ingredient(name)
  content = content_by_name(name)
  return nil if content.blank?
  content.ingredient
end

#next(name = nil) ⇒ Object

Returns next public element from same page.

Pass an element name to get next of this kind.



158
159
160
# File 'app/models/alchemy/element.rb', line 158

def next(name = nil)
  previous_or_next('>', name)
end

#prev(name = nil) ⇒ Object

Returns previous public element from same page.

Pass an element name to get previous of this kind.



166
167
168
# File 'app/models/alchemy/element.rb', line 166

def prev(name = nil)
  previous_or_next('<', name)
end

#richtext_contents_idsObject

Returns an array of all EssenceRichtext contents ids



404
405
406
# File 'app/models/alchemy/element.rb', line 404

def richtext_contents_ids
  contents.essence_richtexts.pluck("#{Content.table_name}.id")
end

#rtf_contentsObject Also known as: richtext_contents



397
398
399
# File 'app/models/alchemy/element.rb', line 397

def rtf_contents
  contents.essence_richtexts
end

#store_page(page) ⇒ Object

Stores the page into touchable_pages (Pages that have to be touched after updating the element).



171
172
173
174
175
176
177
# File 'app/models/alchemy/element.rb', line 171

def store_page(page)
  return true if page.nil?
  unless self.touchable_pages.include? page
    self.touchable_pages << page
    self.save
  end
end

#taggable?Boolean

Returns true if the definition of this element has a taggable true value.

Returns:

  • (Boolean)


420
421
422
# File 'app/models/alchemy/element.rb', line 420

def taggable?
  definition['taggable'] == true
end

#to_partial_pathObject

The element’s view partial is dependent from its name

Define elements

Elements are defined in the config/alchemy/elements.yml file

- name: article
  contents:
  ...

Override the view

Element partials live in app/views/alchemy/elements



438
439
440
# File 'app/models/alchemy/element.rb', line 438

def to_partial_path
  "alchemy/elements/#{name}_view"
end

#trash!Object

Trashing an element means nullifying its position, folding and unpublishing it.



180
181
182
183
184
# File 'app/models/alchemy/element.rb', line 180

def trash!
  self.public = false
  self.folded = true
  self.remove_from_list
end

#trashed?Boolean

Returns:

  • (Boolean)


186
187
188
# File 'app/models/alchemy/element.rb', line 186

def trashed?
  self.position.nil?
end

#update_contents(contents_attributes) ⇒ Boolean

Updates all related contents by calling update_essence on each of them.

Example

@element.update_contents({1 => {ingredient: 'Title'}, 2 => {link: 'https://google.com'}})

Parameters:

  • 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

Returns:

  • (Boolean)

    True if self.errors are blank or contents_attributes hash is nil



286
287
288
289
290
291
292
293
# File 'app/models/alchemy/element.rb', line 286

def update_contents(contents_attributes)
  return true if contents_attributes.nil?
  contents.each do |content|
    content_hash = contents_attributes["#{content.id}"] || next
    content.update_essence(content_hash) || errors.add(:base, :essence_validation_failed)
  end
  errors.blank?
end