Class: Theme

Inherits:
ActiveRecord::Base
  • Object
show all
Includes:
GlobalPath
Defined in:
app/models/theme.rb

Defined Under Namespace

Classes: SettingsMigrationError

Constant Summary collapse

BASE_COMPILER_VERSION =
87

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from GlobalPath

#cdn_path, #cdn_relative_path, #full_cdn_url, #path, #upload_cdn_path

Instance Attribute Details

#child_componentsObject

Returns the value of attribute child_components.



14
15
16
# File 'app/models/theme.rb', line 14

def child_components
  @child_components
end

#skip_child_components_updateObject

Returns the value of attribute skip_child_components_update.



15
16
17
# File 'app/models/theme.rb', line 15

def skip_child_components_update
  @skip_child_components_update
end

Class Method Details

.allowed_remote_theme_idsObject



258
259
260
261
262
263
264
265
# File 'app/models/theme.rb', line 258

def self.allowed_remote_theme_ids
  return nil if GlobalSetting.allowed_theme_repos.blank?

  get_set_cache "allowed_remote_theme_ids" do
    urls = GlobalSetting.allowed_theme_repos.split(",").map(&:strip)
    Theme.joins(:remote_theme).where("remote_themes.remote_url in (?)", urls).pluck(:id)
  end
end

.cacheObject



17
18
19
# File 'app/models/theme.rb', line 17

def self.cache
  @cache ||= DistributedCache.new("theme:compiler:#{BASE_COMPILER_VERSION}")
end

.clear_cache!Object



396
397
398
# File 'app/models/theme.rb', line 396

def self.clear_cache!
  cache.clear
end

.clear_default!Object



282
283
284
285
# File 'app/models/theme.rb', line 282

def self.clear_default!
  SiteSetting.default_theme_id = -1
  expire_site_cache!
end

.compiler_versionObject



204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'app/models/theme.rb', line 204

def self.compiler_version
  get_set_cache "compiler_version" do
    dependencies = [
      BASE_COMPILER_VERSION,
      EmberCli.ember_version,
      GlobalSetting.cdn_url,
      GlobalSetting.s3_cdn_url,
      GlobalSetting.s3_endpoint,
      GlobalSetting.s3_bucket,
      Discourse.current_hostname,
    ]
    Digest::SHA1.hexdigest(dependencies.join)
  end
end

.components_for(theme_id) ⇒ Object



267
268
269
270
271
# File 'app/models/theme.rb', line 267

def self.components_for(theme_id)
  get_set_cache "theme_components_for_#{theme_id}" do
    ChildTheme.where(parent_theme_id: theme_id).pluck(:child_theme_id)
  end
end

.enabled_theme_and_component_idsObject



245
246
247
248
249
250
251
252
253
254
255
256
# File 'app/models/theme.rb', line 245

def self.enabled_theme_and_component_ids
  get_set_cache "enabled_theme_and_component_ids" do
    theme_ids = Theme.user_selectable.where(enabled: true).pluck(:id)
    component_ids =
      ChildTheme
        .where(parent_theme_id: theme_ids)
        .joins(:child_theme)
        .where(themes: { enabled: true })
        .pluck(:child_theme_id)
    (theme_ids | component_ids)
  end
end

.expire_site_cache!Object



273
274
275
276
277
278
279
280
# File 'app/models/theme.rb', line 273

def self.expire_site_cache!
  Site.clear_anon_cache!
  clear_cache!
  ApplicationSerializer.expire_cache_fragment!("user_themes")
  ColorScheme.hex_cache.clear
  CSP::Extension.clear_theme_extensions_cache!
  SvgSprite.expire_cache
end

.get_set_cache(key, &blk) ⇒ Object



219
220
221
# File 'app/models/theme.rb', line 219

def self.get_set_cache(key, &blk)
  cache.defer_get_set(key, &blk)
end

.is_parent_theme?(id) ⇒ Boolean

Returns:

  • (Boolean)


235
236
237
# File 'app/models/theme.rb', line 235

def self.is_parent_theme?(id)
  self.parent_theme_ids.include?(id)
end

.list_baked_fields(theme_ids, target, name) ⇒ Object



527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
# File 'app/models/theme.rb', line 527

def self.list_baked_fields(theme_ids, target, name)
  target = target.to_sym
  name = name&.to_sym

  if target == :translations
    fields = ThemeField.find_first_locale_fields(theme_ids, I18n.fallbacks[name])
  else
    target = :mobile if target == :mobile_theme
    target = :desktop if target == :desktop_theme
    fields =
      ThemeField.find_by_theme_ids(theme_ids).where(
        target_id: [Theme.targets[target], Theme.targets[:common]],
      )
    fields = fields.where(name: name.to_s) unless name.nil?
    fields = fields.order(:target_id)
  end

  fields.each(&:ensure_baked!)
  fields
end

.lookup_field(theme_id, target, field, skip_transformation: false, csp_nonce: nil) ⇒ Object



375
376
377
378
379
380
381
382
# File 'app/models/theme.rb', line 375

def self.lookup_field(theme_id, target, field, skip_transformation: false, csp_nonce: nil)
  return "" if theme_id.blank?

  theme_ids = !skip_transformation ? transform_ids(theme_id) : [theme_id]
  resolved = (resolve_baked_field(theme_ids, target.to_sym, field) || "")
  resolved = resolved.gsub(ThemeField::CSP_NONCE_PLACEHOLDER, csp_nonce) if csp_nonce
  resolved.html_safe
end

.lookup_modifier(theme_ids, modifier_name) ⇒ Object



384
385
386
387
388
389
390
# File 'app/models/theme.rb', line 384

def self.lookup_modifier(theme_ids, modifier_name)
  theme_ids = [theme_ids] unless theme_ids.is_a?(Array)

  get_set_cache("#{theme_ids.join(",")}:modifier:#{modifier_name}:#{Theme.compiler_version}") do
    ThemeModifierSet.resolve_modifier_for_themes(theme_ids, modifier_name)
  end
end

.lookup_target(target_id) ⇒ Object



415
416
417
# File 'app/models/theme.rb', line 415

def self.lookup_target(target_id)
  self.targets.invert[target_id]
end

.notify_theme_change(theme_ids, with_scheme: false, clear_manager_cache: true, all_themes: false) ⇒ Object



419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
# File 'app/models/theme.rb', line 419

def self.notify_theme_change(
  theme_ids,
  with_scheme: false,
  clear_manager_cache: true,
  all_themes: false
)
  Stylesheet::Manager.clear_theme_cache!
  targets = %i[mobile_theme desktop_theme]

  if with_scheme
    targets.prepend(:desktop, :mobile, :admin)
    targets.append(*Discourse.find_plugin_css_assets(mobile_view: true, desktop_view: true))
    Stylesheet::Manager.cache.clear if clear_manager_cache
  end

  if all_themes
    message = theme_ids.map { |id| refresh_message_for_targets(targets, id) }.flatten
  else
    message = refresh_message_for_targets(targets, theme_ids).flatten
  end

  MessageBus.publish("/file-change", message)
end

.parent_theme_idsObject



229
230
231
232
233
# File 'app/models/theme.rb', line 229

def self.parent_theme_ids
  get_set_cache "parent_theme_ids" do
    Theme.where(component: false).pluck(:id)
  end
end

.refresh_message_for_targets(targets, theme_ids) ⇒ Object



450
451
452
453
454
455
456
457
458
# File 'app/models/theme.rb', line 450

def self.refresh_message_for_targets(targets, theme_ids)
  theme_ids = [theme_ids] unless theme_ids.is_a?(Array)

  targets.each_with_object([]) do |target, data|
    theme_ids.each do |theme_id|
      data << Stylesheet::Manager.new(theme_id: theme_id).stylesheet_data(target.to_sym)
    end
  end
end

.remove_from_cache!Object



392
393
394
# File 'app/models/theme.rb', line 392

def self.remove_from_cache!
  clear_cache!
end

.resolve_baked_field(theme_ids, target, name) ⇒ Object



460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
# File 'app/models/theme.rb', line 460

def self.resolve_baked_field(theme_ids, target, name)
  target = target.to_sym
  name = name&.to_sym

  target = :mobile if target == :mobile_theme
  target = :desktop if target == :desktop_theme

  case target
  when :extra_js
    get_set_cache("#{theme_ids.join(",")}:extra_js:#{Theme.compiler_version}") do
      require_rebake =
        ThemeField.where(theme_id: theme_ids, target_id: Theme.targets[:extra_js]).where(
          "compiler_version <> ?",
          Theme.compiler_version,
        )

      ActiveRecord::Base.transaction do
        require_rebake.each { |tf| tf.ensure_baked! }

        Theme.where(id: require_rebake.map(&:theme_id)).each(&:update_javascript_cache!)
      end

      caches =
        JavascriptCache
          .where(theme_id: theme_ids)
          .index_by(&:theme_id)
          .values_at(*theme_ids)
          .compact

      caches.map { |c| <<~HTML.html_safe }.join("\n")
        <script defer src="#{c.url}" data-theme-id="#{c.theme_id}" nonce="#{ThemeField::CSP_NONCE_PLACEHOLDER}"></script>
      HTML
    end
  when :translations
    theme_field_values(theme_ids, :translations, I18n.fallbacks[name])
      .to_a
      .select(&:second)
      .uniq { |((theme_id, _, _), _)| theme_id }
      .flat_map(&:second)
      .join("\n")
  else
    theme_field_values(theme_ids, [:common, target], name).values.compact.flatten.join("\n")
  end
end

.targetsObject



400
401
402
403
404
405
406
407
408
409
410
411
412
413
# File 'app/models/theme.rb', line 400

def self.targets
  @targets ||=
    Enum.new(
      common: 0,
      desktop: 1,
      mobile: 2,
      settings: 3,
      translations: 4,
      extra_scss: 5,
      extra_js: 6,
      tests_js: 7,
      migrations: 8,
    )
end

.theme_field_values(theme_ids, targets, names) ⇒ Object



505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
# File 'app/models/theme.rb', line 505

def self.theme_field_values(theme_ids, targets, names)
  cache.defer_get_set_bulk(
    Array(theme_ids).product(Array(targets), Array(names)),
    lambda do |(theme_id, target, name)|
      "#{theme_id}:#{target}:#{name}:#{Theme.compiler_version}"
    end,
  ) do |keys|
    keys = keys.map { |theme_id, target, name| [theme_id, Theme.targets[target], name.to_s] }

    keys
      .map do |theme_id, target_id, name|
        ThemeField.where(theme_id: theme_id, target_id: target_id, name: name)
      end
      .inject { |a, b| a.or(b) }
      .each(&:ensure_baked!)
      .map { |tf| [[tf.theme_id, tf.target_id, tf.name], tf.value_baked || tf.value] }
      .group_by(&:first)
      .transform_values { |x| x.map(&:second) }
      .values_at(*keys)
  end
end

.theme_idsObject



223
224
225
226
227
# File 'app/models/theme.rb', line 223

def self.theme_ids
  get_set_cache "theme_ids" do
    Theme.pluck(:id)
  end
end

.transform_ids(id) ⇒ Object



287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
# File 'app/models/theme.rb', line 287

def self.transform_ids(id)
  return [] if id.blank?
  id = id.to_i

  get_set_cache "transformed_ids_#{id}" do
    all_ids =
      if self.is_parent_theme?(id)
        components = components_for(id).tap { |c| c.sort!.uniq! }
        [id, *components]
      else
        [id]
      end

    disabled_ids =
      Theme
        .where(id: all_ids)
        .includes(:remote_theme)
        .select { |t| !t.supported? || !t.enabled? }
        .map(&:id)

    all_ids - disabled_ids
  end
end

.user_theme_idsObject



239
240
241
242
243
# File 'app/models/theme.rb', line 239

def self.user_theme_ids
  get_set_cache "user_theme_ids" do
    Theme.user_selectable.pluck(:id)
  end
end

Instance Method Details

#add_relative_theme!(kind, theme) ⇒ Object



624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
# File 'app/models/theme.rb', line 624

def add_relative_theme!(kind, theme)
  new_relation =
    if kind == :child
      child_theme_relation.new(child_theme_id: theme.id)
    else
      parent_theme_relation.new(parent_theme_id: theme.id)
    end
  if new_relation.save
    child_themes.reload
    parent_themes.reload
    save!
    Theme.clear_cache!
  else
    raise Discourse::InvalidParameters.new(new_relation.errors.full_messages.join(", "))
  end
end

#baked_js_tests_with_digestObject



944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
# File 'app/models/theme.rb', line 944

def baked_js_tests_with_digest
  tests_tree =
    theme_fields_to_tree(
      theme_fields.where(target_id: Theme.targets[:tests_js]).order(name: :asc),
    )

  return nil, nil if tests_tree.blank?

  migrations_tree =
    theme_fields_to_tree(
      theme_fields.where(target_id: Theme.targets[:migrations]).order(name: :asc),
    )

  compiler = ThemeJavascriptCompiler.new(id, name, minify: false)
  compiler.append_tree(migrations_tree, include_variables: false)
  compiler.append_tree(tests_tree)

  compiler.append_raw_script "test_setup.js", <<~JS
    (function() {
      require("discourse/lib/theme-settings-store").registerSettings(#{self.id}, #{cached_default_settings.to_json}, { force: true });
    })();
  JS

  content = compiler.content

  if compiler.source_map
    content +=
      "\n//# sourceMappingURL=data:application/json;base64,#{Base64.strict_encode64(compiler.source_map)}\n"
  end

  [content, Digest::SHA1.hexdigest(content)]
end

#build_local_theme_uploads_hashObject



726
727
728
729
730
731
732
733
734
# File 'app/models/theme.rb', line 726

def build_local_theme_uploads_hash
  hash = {}
  upload_fields
    .includes(:javascript_cache, :upload)
    .each do |field|
      hash[field.name] = field.javascript_cache.local_url if field.javascript_cache
    end
  hash
end

#build_settings_hashObject



703
704
705
706
707
708
709
710
711
712
713
714
# File 'app/models/theme.rb', line 703

def build_settings_hash
  hash = {}
  self.settings.each { |name, setting| hash[name] = setting.value }

  theme_uploads = build_theme_uploads_hash
  hash["theme_uploads"] = theme_uploads if theme_uploads.present?

  theme_uploads_local = build_local_theme_uploads_hash
  hash["theme_uploads_local"] = theme_uploads_local if theme_uploads_local.present?

  hash
end

#build_theme_uploads_hashObject



716
717
718
719
720
721
722
723
724
# File 'app/models/theme.rb', line 716

def build_theme_uploads_hash
  hash = {}
  upload_fields
    .includes(:javascript_cache, :upload)
    .each do |field|
      hash[field.name] = Discourse.store.cdn_url(field.upload.url) if field.upload&.url
    end
  hash
end

#cached_default_settingsObject



688
689
690
691
692
693
694
695
696
697
698
699
700
701
# File 'app/models/theme.rb', line 688

def cached_default_settings
  Theme.get_set_cache "default_settings_for_theme_#{self.id}" do
    settings_hash = {}
    self.settings.each { |name, setting| settings_hash[name] = setting.default }

    theme_uploads = build_theme_uploads_hash
    settings_hash["theme_uploads"] = theme_uploads if theme_uploads.present?

    theme_uploads_local = build_local_theme_uploads_hash
    settings_hash["theme_uploads_local"] = theme_uploads_local if theme_uploads_local.present?

    settings_hash
  end
end

#cached_settingsObject



682
683
684
685
686
# File 'app/models/theme.rb', line 682

def cached_settings
  Theme.get_set_cache "settings_for_theme_#{self.id}" do
    build_settings_hash
  end
end

#changed_colorsObject



566
567
568
# File 'app/models/theme.rb', line 566

def changed_colors
  @changed_colors ||= []
end

#changed_fieldsObject



562
563
564
# File 'app/models/theme.rb', line 562

def changed_fields
  @changed_fields ||= []
end

#changed_schemesObject



570
571
572
# File 'app/models/theme.rb', line 570

def changed_schemes
  @changed_schemes ||= Set.new
end

#child_theme_ids=(theme_ids) ⇒ Object



614
615
616
617
# File 'app/models/theme.rb', line 614

def child_theme_ids=(theme_ids)
  super(theme_ids)
  Theme.clear_cache!
end

#component_validationsObject



335
336
337
338
339
340
341
# File 'app/models/theme.rb', line 335

def component_validations
  return unless component

  errors.add(:base, I18n.t("themes.errors.component_no_color_scheme")) if color_scheme_id.present?
  errors.add(:base, I18n.t("themes.errors.component_no_user_selectable")) if user_selectable
  errors.add(:base, I18n.t("themes.errors.component_no_default")) if default?
end

#convert_list_to_json_schema(setting_row, setting) ⇒ Object



921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
# File 'app/models/theme.rb', line 921

def convert_list_to_json_schema(setting_row, setting)
  schema = setting.json_schema
  return if !schema
  keys = schema["items"]["properties"].keys
  return if !keys

  current_values = CSV.parse(setting_row.value, **{ col_sep: "|" }).flatten

  new_values =
    current_values.map do |item|
      parts = CSV.parse(item, **{ col_sep: "," }).flatten
      raise "Schema validation failed" if keys.size < parts.size
      parts.zip(keys).map(&:reverse).to_h
    end

  schemer = JSONSchemer.schema(schema)
  raise "Schema validation failed" if !schemer.valid?(new_values)

  setting_row.value = new_values.to_json
  setting_row.data_type = setting.type
  setting_row.save!
end

#default?Boolean

Returns:

  • (Boolean)


319
320
321
# File 'app/models/theme.rb', line 319

def default?
  SiteSetting.default_theme_id == id
end

#disabled_atObject



824
825
826
# File 'app/models/theme.rb', line 824

def disabled_at
  find_disable_action_log&.created_at
end

#disabled_byObject



820
821
822
# File 'app/models/theme.rb', line 820

def disabled_by
  find_disable_action_log&.acting_user
end

#generate_metadata_hashObject



780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
# File 'app/models/theme.rb', line 780

def 
  {}.tap do |meta|
    meta[:name] = name
    meta[:component] = component

    RemoteTheme::METADATA_PROPERTIES.each do |property|
      meta[property] = remote_theme&.public_send(property)
      meta[property] = nil if meta[property] == "URL" # Clean up old discourse_theme CLI placeholders
    end

    meta[:assets] = {}.tap do |hash|
      theme_fields
        .where(type_id: ThemeField.types[:theme_upload_var])
        .each { |field| hash[field.name] = field.file_path }
    end

    meta[:color_schemes] = {}.tap do |hash|
      schemes = self.color_schemes
      # The selected color scheme may not belong to the theme, so include it anyway
      schemes = [self.color_scheme] + schemes if self.color_scheme
      schemes.uniq.each do |scheme|
        hash[scheme.name] = {}.tap do |colors|
          scheme.colors.each { |color| colors[color.name] = color.hex }
        end
      end
    end

    meta[:modifiers] = {}.tap do |hash|
      ThemeModifierSet.modifiers.keys.each do |modifier|
        value = self.theme_modifier_set.public_send(modifier)
        hash[modifier] = value if !value.nil?
      end
    end

    meta[
      :learn_more
    ] = "https://meta.discourse.org/t/beginners-guide-to-using-discourse-themes/91966"
  end
end

#get_setting(setting_name) ⇒ Object

Retrieves a theme setting

Examples:

theme.get_setting("some_boolean") => True
theme.get_setting("some_string") => "hello"
theme.get_setting(:some_boolean) => True
theme.get_setting(:some_string) => "hello"

Parameters:

  • setting_name (String, Symbol)

    The name of the setting to retrieve.

Returns:

  • (Object)

    The value of the setting that matches the provided name.

Raises:



750
751
752
753
754
# File 'app/models/theme.rb', line 750

def get_setting(setting_name)
  target_setting = settings[setting_name.to_sym]
  raise Discourse::NotFound unless target_setting
  target_setting.value
end

#internal_translationsObject



641
642
643
# File 'app/models/theme.rb', line 641

def internal_translations
  @internal_translations ||= translations(internal: true)
end

#list_baked_fields(target, name) ⇒ Object



552
553
554
555
556
# File 'app/models/theme.rb', line 552

def list_baked_fields(target, name)
  theme_ids = Theme.transform_ids(id)
  theme_ids = [theme_ids.first] if name != :color_definitions
  self.class.list_baked_fields(theme_ids, target, name)
end

#migrate_settings(start_transaction: true, fields: nil, allow_out_of_sequence_migration: false) ⇒ Object



865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
# File 'app/models/theme.rb', line 865

def migrate_settings(start_transaction: true, fields: nil, allow_out_of_sequence_migration: false)
  block = ->(*) do
    runner = ThemeSettingsMigrationsRunner.new(self)
    results =
      runner.run(fields:, raise_error_on_out_of_sequence: !allow_out_of_sequence_migration)

    next if results.blank?

    old_settings = self.theme_settings.pluck(:name)
    self.theme_settings.destroy_all

    final_result = results.last

    final_result[:settings_after].each do |key, val|
      self.update_setting(key.to_sym, val)
    rescue Discourse::NotFound
      if old_settings.include?(key)
        final_result[:settings_after].delete(key)
      else
        raise Theme::SettingsMigrationError.new(
                I18n.t(
                  "themes.import_error.migrations.unknown_setting_returned_by_migration",
                  name: final_result[:original_name],
                  setting_name: key,
                ),
              )
      end
    end

    results.each do |res|
      record =
        ThemeSettingsMigration.new(
          theme_id: self.id,
          version: res[:version],
          name: res[:name],
          theme_field_id: res[:theme_field_id],
        )

      record.calculate_diff(res[:settings_before], res[:settings_after])

      # If out of sequence migration is allowed we don't want to raise an error if the record is invalid due to version
      # conflicts
      allow_out_of_sequence_migration ? record.save : record.save!
    end

    self.reload
    self.update_javascript_cache!
  end

  if start_transaction
    self.transaction(&block)
  else
    block.call
  end
end

#notify_color_change(color, scheme: nil) ⇒ Object



96
97
98
99
100
# File 'app/models/theme.rb', line 96

def notify_color_change(color, scheme: nil)
  scheme ||= color.color_scheme
  changed_colors << color if color
  changed_schemes << scheme if scheme
end

#notify_theme_change(with_scheme: false) ⇒ Object



443
444
445
446
447
448
# File 'app/models/theme.rb', line 443

def notify_theme_change(with_scheme: false)
  DB.after_commit do
    theme_ids = Theme.transform_ids(id)
    self.class.notify_theme_change(theme_ids, with_scheme: with_scheme)
  end
end

#parent_theme_ids=(theme_ids) ⇒ Object



619
620
621
622
# File 'app/models/theme.rb', line 619

def parent_theme_ids=(theme_ids)
  super(theme_ids)
  Theme.clear_cache!
end

#remove_from_cache!Object



558
559
560
# File 'app/models/theme.rb', line 558

def remove_from_cache!
  self.class.remove_from_cache!
end

#repository_urlObject



977
978
979
980
981
982
983
# File 'app/models/theme.rb', line 977

def repository_url
  return unless remote_url
  remote_url.gsub(
    %r{([^@]+@)?(http(s)?://)?(?<host>[^:/]+)[:/](?<path>((?!\.git).)*)(\.git)?(?<rest>.*)},
    '\k<host>/\k<path>\k<rest>',
  )
end

#resolve_baked_field(target, name) ⇒ Object



548
549
550
# File 'app/models/theme.rb', line 548

def resolve_baked_field(target, name)
  list_baked_fields(target, name).map { |f| f.value_baked || f.value }.join("\n")
end

#scss_variablesObject



836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
# File 'app/models/theme.rb', line 836

def scss_variables
  settings_hash = build_settings_hash
  theme_variable_fields = var_theme_fields

  return if theme_variable_fields.empty? && settings_hash.empty?

  contents = +""

  theme_variable_fields&.each do |field|
    if field.type_id == ThemeField.types[:theme_upload_var]
      if upload = field.upload
        url = upload_cdn_path(upload.url)
        contents << "$#{field.name}: unquote(\"#{url}\");"
      else
        contents << "$#{field.name}: unquote(\"\");"
      end
    else
      contents << to_scss_variable(field.name, field.value)
    end
  end

  settings_hash&.each do |name, value|
    next if name == "theme_uploads" || name == "theme_uploads_local"
    contents << to_scss_variable(name, value)
  end

  contents
end

#set_default!Object



311
312
313
314
315
316
317
# File 'app/models/theme.rb', line 311

def set_default!
  if component
    raise Discourse::InvalidParameters.new(I18n.t("themes.errors.component_no_default"))
  end
  SiteSetting.default_theme_id = id
  Theme.expire_site_cache!
end

#set_field(target:, name:, value: nil, type: nil, type_id: nil, upload_id: nil) ⇒ Object



574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
# File 'app/models/theme.rb', line 574

def set_field(target:, name:, value: nil, type: nil, type_id: nil, upload_id: nil)
  name = name.to_s

  target_id = Theme.targets[target.to_sym]
  raise "Unknown target #{target} passed to set field" unless target_id

  type_id ||=
    type ? ThemeField.types[type.to_sym] : ThemeField.guess_type(name: name, target: target)
  raise "Unknown type #{type} passed to set field" unless type_id

  value ||= ""

  field = theme_fields.find_by(name: name, target_id: target_id, type_id: type_id)

  if field
    if value.blank? && !upload_id
      field.destroy
    else
      if field.value != value || field.upload_id != upload_id
        field.value = value
        field.upload_id = upload_id
        changed_fields << field
      end
    end
  else
    if value.present? || upload_id.present?
      field =
        theme_fields.build(
          target_id: target_id,
          value: value,
          name: name,
          type_id: type_id,
          upload_id: upload_id,
        )
      changed_fields << field
    end
  end
  field
end

#settingsObject



667
668
669
670
671
672
673
674
675
676
677
678
679
680
# File 'app/models/theme.rb', line 667

def settings
  field = settings_field
  settings = {}

  if field && field.error.nil?
    ThemeSettingsParser
      .new(field)
      .load do |name, default, type, opts|
        settings[name] = ThemeSettingsManager.create(name, default, type, self, opts)
      end
  end

  settings
end

#should_refresh_development_clients?Boolean

Returns:

  • (Boolean)


151
152
153
# File 'app/models/theme.rb', line 151

def should_refresh_development_clients?
  Rails.env.development?
end

#supported?Boolean

Returns:

  • (Boolean)


323
324
325
326
327
328
329
330
331
332
333
# File 'app/models/theme.rb', line 323

def supported?
  if minimum_version = remote_theme&.minimum_discourse_version
    return false unless Discourse.has_needed_version?(Discourse::VERSION::STRING, minimum_version)
  end

  if maximum_version = remote_theme&.maximum_discourse_version
    return false unless Discourse.has_needed_version?(maximum_version, Discourse::VERSION::STRING)
  end

  true
end

#switch_to_component!Object



349
350
351
352
353
354
355
356
357
358
359
360
361
362
# File 'app/models/theme.rb', line 349

def switch_to_component!
  return if component

  Theme.transaction do
    self.component = true

    self.color_scheme_id = nil
    self.user_selectable = false
    Theme.clear_default! if default?

    ChildTheme.where("parent_theme_id = ?", id).destroy_all
    self.save!
  end
end

#switch_to_theme!Object



364
365
366
367
368
369
370
371
372
373
# File 'app/models/theme.rb', line 364

def switch_to_theme!
  return unless component

  Theme.transaction do
    self.enabled = true
    self.component = false
    ChildTheme.where("child_theme_id = ?", id).destroy_all
    self.save!
  end
end

#theme_modifier_setObject



102
103
104
# File 'app/models/theme.rb', line 102

def theme_modifier_set
  super || build_theme_modifier_set
end

#translation_override_hashObject



769
770
771
772
773
774
775
776
777
778
# File 'app/models/theme.rb', line 769

def translation_override_hash
  hash = {}
  theme_translation_overrides.each do |override|
    cursor = hash
    path = [override.locale] + override.translation_key.split(".")
    path[0..-2].each { |key| cursor = (cursor[key] ||= {}) }
    cursor[path[-1]] = override.value
  end
  hash
end

#translations(internal: false) ⇒ Object



645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
# File 'app/models/theme.rb', line 645

def translations(internal: false)
  fallbacks = I18n.fallbacks[I18n.locale]
  begin
    data =
      locale_fields.first&.translation_data(
        with_overrides: false,
        internal: internal,
        fallback_fields: locale_fields,
      )
    return {} if data.nil?
    best_translations = {}
    fallbacks.reverse.each { |locale| best_translations.deep_merge! data[locale] if data[locale] }
    ThemeTranslationManager.list_from_hash(
      theme: self,
      hash: best_translations,
      locale: I18n.locale,
    )
  rescue ThemeTranslationParser::InvalidYaml
    {}
  end
end

#update_child_componentsObject



155
156
157
158
159
160
161
162
163
164
# File 'app/models/theme.rb', line 155

def update_child_components
  if !component? && child_components.present? && !skip_child_components_update
    child_components.each do |url|
      url = ThemeStore::GitImporter.new(url.strip).url
      theme = RemoteTheme.find_by(remote_url: url)&.theme
      theme ||= RemoteTheme.import_theme(url, user)
      child_themes << theme
    end
  end
end

#update_javascript_cache!Object



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'app/models/theme.rb', line 166

def update_javascript_cache!
  all_extra_js =
    theme_fields
      .where(target_id: Theme.targets[:extra_js])
      .order(:name, :id)
      .pluck(:name, :value)
      .to_h

  if all_extra_js.present?
    js_compiler = ThemeJavascriptCompiler.new(id, name)
    js_compiler.append_tree(all_extra_js)
    settings_hash = build_settings_hash

    js_compiler.prepend_settings(settings_hash) if settings_hash.present?

    javascript_cache || build_javascript_cache
    javascript_cache.update!(content: js_compiler.content, source_map: js_compiler.source_map)
  else
    javascript_cache&.destroy!
  end
end

#update_setting(setting_name, new_value) ⇒ Object



756
757
758
759
760
761
# File 'app/models/theme.rb', line 756

def update_setting(setting_name, new_value)
  target_setting = settings[setting_name.to_sym]
  raise Discourse::NotFound unless target_setting
  target_setting.value = new_value
  self.theme_setting_requests_refresh = true if target_setting.requests_refresh?
end

#update_translation(translation_key, new_value) ⇒ Object



763
764
765
766
767
# File 'app/models/theme.rb', line 763

def update_translation(translation_key, new_value)
  target_translation = translations.find { |translation| translation.key == translation_key }
  raise Discourse::NotFound unless target_translation
  target_translation.value = new_value
end

#user_selectable_countObject



985
986
987
# File 'app/models/theme.rb', line 985

def user_selectable_count
  UserOption.where(theme_ids: [id]).count
end

#validate_theme_fieldsObject



343
344
345
346
347
# File 'app/models/theme.rb', line 343

def validate_theme_fields
  theme_fields.each do |field|
    field.errors.full_messages.each { |message| errors.add(:base, message) } unless field.valid?
  end
end

#with_scss_load_pathsObject



828
829
830
831
832
833
834
# File 'app/models/theme.rb', line 828

def with_scss_load_paths
  return yield([]) if self.extra_scss_fields.empty?

  ThemeStore::ZipExporter
    .new(self)
    .with_export_dir(extra_scss_only: true) { |dir| yield ["#{dir}/stylesheets"] }
end