Class: LatoCms::Page

Inherits:
ApplicationRecord
  • Object
show all
Includes:
LatoSpaces::Associable, LatoSpaces::AssociableRequired, LatoSpaces::AssociableUnique
Defined in:
app/models/lato_cms/page.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#actionsObject

Returns the value of attribute actions.



7
8
9
# File 'app/models/lato_cms/page.rb', line 7

def actions
  @actions
end

Class Method Details

.parse_translated_field_values(content, expected_count) ⇒ Object



109
110
111
112
113
114
115
116
117
118
# File 'app/models/lato_cms/page.rb', line 109

def self.parse_translated_field_values(content, expected_count)
  return [] if content.blank?

  parsed = JSON.parse(content[/\[.*\]/m] || content)
  return [] unless parsed.is_a?(Array) && parsed.size == expected_count

  parsed.map(&:to_s)
rescue JSON::ParserError
  []
end

.translate_fields_prompt(fields, target_locale) ⇒ Object



103
104
105
106
107
# File 'app/models/lato_cms/page.rb', line 103

def self.translate_fields_prompt(fields, target_locale)
  "Translate each string in this JSON array into #{target_locale} (language code). Preserve any HTML " \
  "tags exactly as-is, translate only the visible text. Respond with a JSON array of the same length " \
  "and in the same order, translated strings only, no markdown, no explanation.\n\n#{fields.map(&:value).to_json}"
end

Instance Method Details

#as_json(options = {}) ⇒ Object



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'app/models/lato_cms/page.rb', line 160

def as_json(options = {})
  data = {
    id: id,
    title: title,
    permalink: permalink,
    locale: locale,
    template_id: template_id,
    template_name: template_name,
    frontend_url: frontend_url,
    translations: translations_json,
    created_at: created_at,
    updated_at: updated_at
  }

  if options[:include_fields]
    fields.load unless fields.loaded?
    data[:fields] = build_fields_json(template_components.select { |tc| component_effectively_enabled?(tc[:template_component_id]) })
  end

  data
end

#component_effectively_enabled?(template_component_id, default: true) ⇒ Boolean

Returns:

  • (Boolean)


149
150
151
152
153
# File 'app/models/lato_cms/page.rb', line 149

def component_effectively_enabled?(template_component_id, default: true)
  return true if component_required?(template_component_id)

  component_enabled?(template_component_id, default: default)
end

#component_enabled?(template_component_id, default: true) ⇒ Boolean

Returns:

  • (Boolean)


137
138
139
140
141
142
# File 'app/models/lato_cms/page.rb', line 137

def component_enabled?(template_component_id, default: true)
  value = (component_states || {})[template_component_id.to_s]
  return default if value.nil?

  value == true
end

#component_required?(template_component_id) ⇒ Boolean

Returns:

  • (Boolean)


144
145
146
147
# File 'app/models/lato_cms/page.rb', line 144

def component_required?(template_component_id)
  tc = template_components.find { |component| component[:template_component_id] == template_component_id.to_s }
  tc && tc[:required] == true
end

Links another page as a translation of this one, merging any existing groups so all members share a single translation_group_id.



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'app/models/lato_cms/page.rb', line 49

def link_translation(other_page)
  return false unless other_page && other_page.id != id
  return false unless other_page.lato_spaces_group_id == lato_spaces_group_id

  group_id = translation_group_id.presence || other_page.translation_group_id.presence || SecureRandom.uuid
  old_group_id = other_page.translation_group_id.presence

  transaction do
    update!(translation_group_id: group_id)
    # Re-point every member of the other page's former group to keep groups merged.
    if old_group_id && old_group_id != group_id
      LatoCms::Page
        .for_lato_spaces_group(lato_spaces_group_id)
        .where(translation_group_id: old_group_id)
        .update_all(translation_group_id: group_id)
    else
      other_page.update!(translation_group_id: group_id)
    end
  end

  true
end

#set_component_enabled(template_component_id, enabled) ⇒ Object



155
156
157
158
# File 'app/models/lato_cms/page.rb', line 155

def set_component_enabled(template_component_id, enabled)
  key = template_component_id.to_s
  self.component_states = (component_states || {}).merge(key => ActiveModel::Type::Boolean.new.cast(enabled))
end

#templateObject



120
121
122
# File 'app/models/lato_cms/page.rb', line 120

def template
  LatoCms::TemplateManager.find_template(template_id)
end

#template_available?Boolean

Returns:

  • (Boolean)


128
129
130
# File 'app/models/lato_cms/page.rb', line 128

def template_available?
  template_id.blank? || template.present?
end

#template_componentsObject



132
133
134
135
# File 'app/models/lato_cms/page.rb', line 132

def template_components
  return [] unless template
  LatoCms::TemplateManager.resolve_template_components(template)
end

#template_nameObject



124
125
126
# File 'app/models/lato_cms/page.rb', line 124

def template_name
  template&.dig('name') || template_id
end

#translate_component_fields!(template_component_id) ⇒ Object

Translates a component's free-text fields (see LatoCms::PageField::TRANSLATABLE_FIELD_TYPES) into this page's own locale via the configured LLM, in one batched call, overwriting their values in place. Used by PagesController#clone_component_action's "Clone & translate" option, as the async half run via LatoCms::TranslateComponentFieldsJob (a Lato::Operation): the fields themselves are already cloned by the time this runs, synchronously, so a failure here never loses the clone, only the translation. Raises on failure (LLM unreachable, malformed/short response) rather than swallowing it, since the admin is watching the operation's progress and a silent no-op would misreport success.



91
92
93
94
95
96
97
98
99
100
101
# File 'app/models/lato_cms/page.rb', line 91

def translate_component_fields!(template_component_id)
  targets = fields.where(template_component_id: template_component_id).reject(&:repeater_order?)
    .select { |f| LatoCms::PageField::TRANSLATABLE_FIELD_TYPES.include?(f.field_type) && f.value.present? }
  return if targets.empty?

  content = LatoCms::LlmClient.chat(messages: [{ role: 'user', content: self.class.translate_fields_prompt(targets, locale) }])
  translated = self.class.parse_translated_field_values(content, targets.size)
  raise "The LLM response couldn't be parsed into #{targets.size} translated value(s)" if translated.blank?

  targets.each_with_index { |field, index| field.update_column(:value, translated[index]) if translated[index].present? }
end

#translation_candidates(locale) ⇒ Object

Pages of a given locale that can be linked to this page: same spaces group, not this page, and not already part of any translation group.



40
41
42
43
44
45
# File 'app/models/lato_cms/page.rb', line 40

def translation_candidates(locale)
  LatoCms::Page
    .for_lato_spaces_group(lato_spaces_group_id)
    .where(locale: locale.to_s, translation_group_id: nil)
    .where.not(id: id)
end

#translation_for(locale) ⇒ Object

The linked translation for a given locale, if any.



34
35
36
# File 'app/models/lato_cms/page.rb', line 34

def translation_for(locale)
  translations.find_by(locale: locale.to_s)
end

#translationsObject

Sibling pages linked as translations of this page.



24
25
26
27
28
29
30
31
# File 'app/models/lato_cms/page.rb', line 24

def translations
  return LatoCms::Page.none if translation_group_id.blank?

  LatoCms::Page
    .for_lato_spaces_group(lato_spaces_group_id)
    .where(translation_group_id: translation_group_id)
    .where.not(id: id)
end

Removes a page from this page's translation group.



73
74
75
76
77
78
# File 'app/models/lato_cms/page.rb', line 73

def unlink_translation(other_page)
  return false unless other_page && other_page.translation_group_id == translation_group_id

  other_page.update!(translation_group_id: nil)
  true
end