2019-04-30 16:02:55 +08:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2021-03-11 09:15:04 +08:00
|
|
|
require "csv"
|
|
|
|
require "json_schemer"
|
2021-02-03 02:09:41 +08:00
|
|
|
|
2017-04-12 22:52:52 +08:00
|
|
|
class Theme < ActiveRecord::Base
|
2021-02-03 02:09:41 +08:00
|
|
|
include GlobalPath
|
2018-07-12 12:18:21 +08:00
|
|
|
|
2024-12-11 22:09:25 +08:00
|
|
|
BASE_COMPILER_VERSION = 86
|
2022-07-14 19:43:55 +08:00
|
|
|
|
FEATURE: Theme settings migrations (#24071)
This commit introduces a new feature that allows theme developers to manage the transformation of theme settings over time. Similar to Rails migrations, the theme settings migration system enables developers to write and execute migrations for theme settings, ensuring a smooth transition when changes are required in the format or structure of setting values.
Example use cases for the theme settings migration system:
1. Renaming a theme setting.
2. Changing the data type of a theme setting (e.g., transforming a string setting containing comma-separated values into a proper list setting).
3. Altering the format of data stored in a theme setting.
All of these use cases and more are now possible while preserving theme setting values for sites that have already modified their theme settings.
Usage:
1. Create a top-level directory called `migrations` in your theme/component, and then within the `migrations` directory create another directory called `settings`.
2. Inside the `migrations/settings` directory, create a JavaScript file using the format `XXXX-some-name.js`, where `XXXX` is a unique 4-digit number, and `some-name` is a descriptor of your choice that describes the migration.
3. Within the JavaScript file, define and export (as the default) a function called `migrate`. This function will receive a `Map` object and must also return a `Map` object (it's acceptable to return the same `Map` object that the function received).
4. The `Map` object received by the `migrate` function will include settings that have been overridden or changed by site administrators. Settings that have never been changed from the default will not be included.
5. The keys and values contained in the `Map` object that the `migrate` function returns will replace all the currently changed settings of the theme.
6. Migrations are executed in numerical order based on the XXXX segment in the migration filenames. For instance, `0001-some-migration.js` will be executed before `0002-another-migration.js`.
Here's a complete example migration script that renames a setting from `setting_with_old_name` to `setting_with_new_name`:
```js
// File name: 0001-rename-setting.js
export default function migrate(settings) {
if (settings.has("setting_with_old_name")) {
settings.set("setting_with_new_name", settings.get("setting_with_old_name"));
}
return settings;
}
```
Internal topic: t/109980
2023-11-02 13:10:15 +08:00
|
|
|
class SettingsMigrationError < StandardError
|
|
|
|
end
|
|
|
|
|
2020-03-05 20:58:18 +08:00
|
|
|
attr_accessor :child_components
|
2024-07-25 22:10:51 +08:00
|
|
|
attr_accessor :skip_child_components_update
|
2020-03-05 20:58:18 +08:00
|
|
|
|
2023-08-18 23:59:11 +08:00
|
|
|
def self.cache
|
|
|
|
@cache ||= DistributedCache.new("theme:compiler:#{BASE_COMPILER_VERSION}")
|
|
|
|
end
|
2017-04-12 22:52:52 +08:00
|
|
|
|
2018-05-18 16:09:21 +08:00
|
|
|
belongs_to :user
|
2017-04-12 22:52:52 +08:00
|
|
|
belongs_to :color_scheme
|
2023-06-14 01:07:47 +08:00
|
|
|
has_many :theme_fields, dependent: :destroy, validate: false
|
2018-03-05 08:04:23 +08:00
|
|
|
has_many :theme_settings, dependent: :destroy
|
2019-01-17 19:46:11 +08:00
|
|
|
has_many :theme_translation_overrides, dependent: :destroy
|
2017-04-12 22:52:52 +08:00
|
|
|
has_many :child_theme_relation,
|
|
|
|
class_name: "ChildTheme",
|
|
|
|
foreign_key: "parent_theme_id",
|
|
|
|
dependent: :destroy
|
2019-01-23 17:20:13 +08:00
|
|
|
has_many :parent_theme_relation,
|
|
|
|
class_name: "ChildTheme",
|
|
|
|
foreign_key: "child_theme_id",
|
|
|
|
dependent: :destroy
|
2018-12-21 01:13:05 +08:00
|
|
|
has_many :child_themes, -> { order(:name) }, through: :child_theme_relation, source: :child_theme
|
2019-01-23 17:20:13 +08:00
|
|
|
has_many :parent_themes,
|
|
|
|
-> { order(:name) },
|
|
|
|
through: :parent_theme_relation,
|
|
|
|
source: :parent_theme
|
2017-04-18 03:56:13 +08:00
|
|
|
has_many :color_schemes
|
FEATURE: Theme settings migrations (#24071)
This commit introduces a new feature that allows theme developers to manage the transformation of theme settings over time. Similar to Rails migrations, the theme settings migration system enables developers to write and execute migrations for theme settings, ensuring a smooth transition when changes are required in the format or structure of setting values.
Example use cases for the theme settings migration system:
1. Renaming a theme setting.
2. Changing the data type of a theme setting (e.g., transforming a string setting containing comma-separated values into a proper list setting).
3. Altering the format of data stored in a theme setting.
All of these use cases and more are now possible while preserving theme setting values for sites that have already modified their theme settings.
Usage:
1. Create a top-level directory called `migrations` in your theme/component, and then within the `migrations` directory create another directory called `settings`.
2. Inside the `migrations/settings` directory, create a JavaScript file using the format `XXXX-some-name.js`, where `XXXX` is a unique 4-digit number, and `some-name` is a descriptor of your choice that describes the migration.
3. Within the JavaScript file, define and export (as the default) a function called `migrate`. This function will receive a `Map` object and must also return a `Map` object (it's acceptable to return the same `Map` object that the function received).
4. The `Map` object received by the `migrate` function will include settings that have been overridden or changed by site administrators. Settings that have never been changed from the default will not be included.
5. The keys and values contained in the `Map` object that the `migrate` function returns will replace all the currently changed settings of the theme.
6. Migrations are executed in numerical order based on the XXXX segment in the migration filenames. For instance, `0001-some-migration.js` will be executed before `0002-another-migration.js`.
Here's a complete example migration script that renames a setting from `setting_with_old_name` to `setting_with_new_name`:
```js
// File name: 0001-rename-setting.js
export default function migrate(settings) {
if (settings.has("setting_with_old_name")) {
settings.set("setting_with_new_name", settings.get("setting_with_old_name"));
}
return settings;
}
```
Internal topic: t/109980
2023-11-02 13:10:15 +08:00
|
|
|
has_many :theme_settings_migrations
|
2019-08-29 22:47:08 +08:00
|
|
|
belongs_to :remote_theme, dependent: :destroy
|
2020-03-11 21:30:45 +08:00
|
|
|
has_one :theme_modifier_set, dependent: :destroy
|
2023-03-15 02:11:45 +08:00
|
|
|
has_one :theme_svg_sprite, dependent: :destroy
|
2017-04-12 22:52:52 +08:00
|
|
|
|
2018-12-21 01:13:05 +08:00
|
|
|
has_one :settings_field,
|
|
|
|
-> { where(target_id: Theme.targets[:settings], name: "yaml") },
|
|
|
|
class_name: "ThemeField"
|
2019-06-03 17:41:00 +08:00
|
|
|
has_one :javascript_cache, dependent: :destroy
|
2019-02-26 22:22:02 +08:00
|
|
|
has_many :locale_fields,
|
|
|
|
-> { filter_locale_fields(I18n.fallbacks[I18n.locale]) },
|
|
|
|
class_name: "ThemeField"
|
2020-05-02 00:51:11 +08:00
|
|
|
has_many :upload_fields,
|
|
|
|
-> { where(type_id: ThemeField.types[:theme_upload_var]).preload(:upload) },
|
|
|
|
class_name: "ThemeField"
|
2021-02-03 02:09:41 +08:00
|
|
|
has_many :extra_scss_fields,
|
|
|
|
-> { where(target_id: Theme.targets[:extra_scss]) },
|
|
|
|
class_name: "ThemeField"
|
2021-06-15 14:57:17 +08:00
|
|
|
has_many :yaml_theme_fields,
|
|
|
|
-> { where("name = 'yaml' AND type_id = ?", ThemeField.types[:yaml]) },
|
|
|
|
class_name: "ThemeField"
|
|
|
|
has_many :var_theme_fields,
|
|
|
|
-> { where("type_id IN (?)", ThemeField.theme_var_type_ids) },
|
|
|
|
class_name: "ThemeField"
|
|
|
|
has_many :builder_theme_fields,
|
|
|
|
-> { where("name IN (?)", ThemeField.scss_fields) },
|
|
|
|
class_name: "ThemeField"
|
FEATURE: Theme settings migrations (#24071)
This commit introduces a new feature that allows theme developers to manage the transformation of theme settings over time. Similar to Rails migrations, the theme settings migration system enables developers to write and execute migrations for theme settings, ensuring a smooth transition when changes are required in the format or structure of setting values.
Example use cases for the theme settings migration system:
1. Renaming a theme setting.
2. Changing the data type of a theme setting (e.g., transforming a string setting containing comma-separated values into a proper list setting).
3. Altering the format of data stored in a theme setting.
All of these use cases and more are now possible while preserving theme setting values for sites that have already modified their theme settings.
Usage:
1. Create a top-level directory called `migrations` in your theme/component, and then within the `migrations` directory create another directory called `settings`.
2. Inside the `migrations/settings` directory, create a JavaScript file using the format `XXXX-some-name.js`, where `XXXX` is a unique 4-digit number, and `some-name` is a descriptor of your choice that describes the migration.
3. Within the JavaScript file, define and export (as the default) a function called `migrate`. This function will receive a `Map` object and must also return a `Map` object (it's acceptable to return the same `Map` object that the function received).
4. The `Map` object received by the `migrate` function will include settings that have been overridden or changed by site administrators. Settings that have never been changed from the default will not be included.
5. The keys and values contained in the `Map` object that the `migrate` function returns will replace all the currently changed settings of the theme.
6. Migrations are executed in numerical order based on the XXXX segment in the migration filenames. For instance, `0001-some-migration.js` will be executed before `0002-another-migration.js`.
Here's a complete example migration script that renames a setting from `setting_with_old_name` to `setting_with_new_name`:
```js
// File name: 0001-rename-setting.js
export default function migrate(settings) {
if (settings.has("setting_with_old_name")) {
settings.set("setting_with_new_name", settings.get("setting_with_old_name"));
}
return settings;
}
```
Internal topic: t/109980
2023-11-02 13:10:15 +08:00
|
|
|
has_many :migration_fields,
|
|
|
|
-> { where(target_id: Theme.targets[:migrations]) },
|
|
|
|
class_name: "ThemeField"
|
2018-12-21 01:13:05 +08:00
|
|
|
|
2018-08-24 09:30:00 +08:00
|
|
|
validate :component_validations
|
2023-06-14 01:07:47 +08:00
|
|
|
validate :validate_theme_fields
|
2018-08-08 12:46:34 +08:00
|
|
|
|
2020-03-27 06:11:56 +08:00
|
|
|
after_create :update_child_components
|
|
|
|
|
2018-08-08 12:46:34 +08:00
|
|
|
scope :user_selectable, -> { where("user_selectable OR id = ?", SiteSetting.default_theme_id) }
|
2023-01-09 20:20:10 +08:00
|
|
|
|
2021-04-27 19:30:29 +08:00
|
|
|
scope :include_relations,
|
2023-11-29 13:38:07 +08:00
|
|
|
-> do
|
2021-04-27 19:30:29 +08:00
|
|
|
includes(
|
|
|
|
:child_themes,
|
|
|
|
:parent_themes,
|
|
|
|
:remote_theme,
|
|
|
|
:theme_settings,
|
|
|
|
:settings_field,
|
|
|
|
:locale_fields,
|
|
|
|
:user,
|
|
|
|
:color_scheme,
|
2021-11-12 01:11:23 +08:00
|
|
|
:theme_translation_overrides,
|
2024-01-11 14:04:02 +08:00
|
|
|
theme_fields: %i[upload theme_settings_migration],
|
2021-04-27 19:30:29 +08:00
|
|
|
)
|
2023-11-29 13:38:07 +08:00
|
|
|
end
|
2021-04-27 19:30:29 +08:00
|
|
|
|
2024-03-21 00:00:43 +08:00
|
|
|
delegate :remote_url, to: :remote_theme, private: true, allow_nil: true
|
|
|
|
|
2019-08-12 18:02:38 +08:00
|
|
|
def notify_color_change(color, scheme: nil)
|
|
|
|
scheme ||= color.color_scheme
|
|
|
|
changed_colors << color if color
|
|
|
|
changed_schemes << scheme if scheme
|
2017-04-18 03:56:13 +08:00
|
|
|
end
|
|
|
|
|
2020-03-11 21:30:45 +08:00
|
|
|
def theme_modifier_set
|
|
|
|
super || build_theme_modifier_set
|
|
|
|
end
|
|
|
|
|
2017-04-12 22:52:52 +08:00
|
|
|
after_save do
|
2019-08-12 18:02:38 +08:00
|
|
|
changed_colors.each(&:save!)
|
|
|
|
changed_schemes.each(&:save!)
|
2018-03-15 15:26:54 +08:00
|
|
|
|
2017-04-18 03:56:13 +08:00
|
|
|
changed_colors.clear
|
2019-08-12 18:02:38 +08:00
|
|
|
changed_schemes.clear
|
2018-03-15 15:26:54 +08:00
|
|
|
|
2023-08-04 18:02:26 +08:00
|
|
|
any_non_css_fields_changed =
|
|
|
|
changed_fields.any? { |f| !(f.basic_scss_field? || f.extra_scss_field?) }
|
|
|
|
|
2017-04-12 22:52:52 +08:00
|
|
|
changed_fields.each(&:save!)
|
|
|
|
changed_fields.clear
|
|
|
|
|
2020-03-11 21:30:45 +08:00
|
|
|
theme_modifier_set.save!
|
|
|
|
|
2019-06-03 17:41:00 +08:00
|
|
|
theme_fields.select(&:basic_html_field?).each(&:invalidate_baked!) if saved_change_to_name?
|
|
|
|
|
2021-01-08 00:15:38 +08:00
|
|
|
if saved_change_to_color_scheme_id? || saved_change_to_user_selectable? || saved_change_to_name?
|
|
|
|
Theme.expire_site_cache!
|
2023-01-09 20:20:10 +08:00
|
|
|
end
|
2019-05-08 23:02:55 +08:00
|
|
|
notify_with_scheme = saved_change_to_color_scheme_id?
|
2017-04-12 22:52:52 +08:00
|
|
|
|
2019-04-12 18:36:08 +08:00
|
|
|
reload
|
|
|
|
settings_field&.ensure_baked! # Other fields require setting to be **baked**
|
|
|
|
theme_fields.each(&:ensure_baked!)
|
|
|
|
|
2019-07-16 22:34:33 +08:00
|
|
|
update_javascript_cache!
|
|
|
|
|
|
|
|
remove_from_cache!
|
2023-07-12 22:49:28 +08:00
|
|
|
ColorScheme.hex_cache.clear
|
2024-01-11 14:04:02 +08:00
|
|
|
|
2019-07-16 22:34:33 +08:00
|
|
|
notify_theme_change(with_scheme: notify_with_scheme)
|
2021-11-22 20:16:56 +08:00
|
|
|
|
|
|
|
if theme_setting_requests_refresh
|
|
|
|
DB.after_commit do
|
|
|
|
Discourse.request_refresh!
|
|
|
|
self.theme_setting_requests_refresh = false
|
|
|
|
end
|
|
|
|
end
|
2023-08-04 18:02:26 +08:00
|
|
|
|
|
|
|
if any_non_css_fields_changed && should_refresh_development_clients?
|
|
|
|
MessageBus.publish "/file-change", ["development-mode-theme-changed"]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def should_refresh_development_clients?
|
|
|
|
Rails.env.development?
|
2019-07-16 22:34:33 +08:00
|
|
|
end
|
|
|
|
|
2020-03-27 06:11:56 +08:00
|
|
|
def update_child_components
|
2024-07-25 22:10:51 +08:00
|
|
|
if !component? && child_components.present? && !skip_child_components_update
|
2020-03-05 20:58:18 +08:00
|
|
|
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
|
|
|
|
|
2019-07-16 22:34:33 +08:00
|
|
|
def update_javascript_cache!
|
FIX: Ensure theme JavaScript cache get consistent SHA1 digest (#15933)
There is a couple of layers of caching for theme JavaScript in Discourse:
The first layer is the `javascript_caches` table in the database. When a theme
with JavaScript files is installed, Discourse stores each one of the JavaScript
files in the `theme_fields` table, and then concatenates the files, compiles
them, computes a SHA1 digest of the compiled JavaScript and store the results
along with the SHA1 digest in the `javascript_caches` table.
Now when a request comes in, we need to render `<script>` tags for the
activated theme(s) of the site. To do this, we retrieve the `javascript_caches`
records of the activated themes and generate a `<script>` tag for each record.
The `src` attribute of these tags is a path to the `/theme-javascripts/:digest`
route which simply responds with the compiled JavaScript that has the requested
digest.
The second layer is a distributed cache whose purpose is to make rendering
`<script>` a lot more efficient. Without this cache, we'd have to query the
`javascript_caches` table to retrieve the SHA1 digests for every single
request. So we use this cache to store the `<script>` tags themselves so that
we only have to retrieve the `javascript_caches` records of the activated
themes for the first request and future requests simply get the cached
`<script>` tags.
What this commit does it ensures that the SHA1 digest in the
`javascript_caches` table stay the same across compilations by adding an order
by id clause to the query that loads the `theme_fields` records. Currently, we
specify no order when retrieving the `theme_fields` records so the order in
which they're retrieved can change across compilations and therefore cause the
SHA1 to change even though the individual records have not changed at all.
An inconsistent SHA1 digest across compilations can cause the database cache
and the distributed cache to have different digests and that causes the
JavaScript to fail to load (and if the theme heavily customizes the site, it
gives the impression that the site is broken) until the cache is cleared.
This can happen in busy sites when 2 concurrent requests recompile the
JavaScript files of a theme at the same time (this can happen when deploying a
new Discourse version) and request A updates the database cache after request B
did, and request B updates the distributed cache after request A did.
Internal ticket: t60783.
Co-authored-by: David Taylor <david@taylorhq.com>
2022-02-14 21:23:06 +08:00
|
|
|
all_extra_js =
|
|
|
|
theme_fields
|
|
|
|
.where(target_id: Theme.targets[:extra_js])
|
|
|
|
.order(:name, :id)
|
2022-10-17 22:04:04 +08:00
|
|
|
.pluck(:name, :value)
|
|
|
|
.to_h
|
FIX: Ensure theme JavaScript cache get consistent SHA1 digest (#15933)
There is a couple of layers of caching for theme JavaScript in Discourse:
The first layer is the `javascript_caches` table in the database. When a theme
with JavaScript files is installed, Discourse stores each one of the JavaScript
files in the `theme_fields` table, and then concatenates the files, compiles
them, computes a SHA1 digest of the compiled JavaScript and store the results
along with the SHA1 digest in the `javascript_caches` table.
Now when a request comes in, we need to render `<script>` tags for the
activated theme(s) of the site. To do this, we retrieve the `javascript_caches`
records of the activated themes and generate a `<script>` tag for each record.
The `src` attribute of these tags is a path to the `/theme-javascripts/:digest`
route which simply responds with the compiled JavaScript that has the requested
digest.
The second layer is a distributed cache whose purpose is to make rendering
`<script>` a lot more efficient. Without this cache, we'd have to query the
`javascript_caches` table to retrieve the SHA1 digests for every single
request. So we use this cache to store the `<script>` tags themselves so that
we only have to retrieve the `javascript_caches` records of the activated
themes for the first request and future requests simply get the cached
`<script>` tags.
What this commit does it ensures that the SHA1 digest in the
`javascript_caches` table stay the same across compilations by adding an order
by id clause to the query that loads the `theme_fields` records. Currently, we
specify no order when retrieving the `theme_fields` records so the order in
which they're retrieved can change across compilations and therefore cause the
SHA1 to change even though the individual records have not changed at all.
An inconsistent SHA1 digest across compilations can cause the database cache
and the distributed cache to have different digests and that causes the
JavaScript to fail to load (and if the theme heavily customizes the site, it
gives the impression that the site is broken) until the cache is cleared.
This can happen in busy sites when 2 concurrent requests recompile the
JavaScript files of a theme at the same time (this can happen when deploying a
new Discourse version) and request A updates the database cache after request B
did, and request B updates the distributed cache after request A did.
Internal ticket: t60783.
Co-authored-by: David Taylor <david@taylorhq.com>
2022-02-14 21:23:06 +08:00
|
|
|
|
2019-06-03 17:41:00 +08:00
|
|
|
if all_extra_js.present?
|
|
|
|
js_compiler = ThemeJavascriptCompiler.new(id, name)
|
2022-10-17 22:04:04 +08:00
|
|
|
js_compiler.append_tree(all_extra_js)
|
2020-05-02 00:51:11 +08:00
|
|
|
settings_hash = build_settings_hash
|
2024-02-05 14:35:11 +08:00
|
|
|
|
2020-05-02 00:51:11 +08:00
|
|
|
js_compiler.prepend_settings(settings_hash) if settings_hash.present?
|
2024-02-05 14:35:11 +08:00
|
|
|
|
2019-06-03 17:41:00 +08:00
|
|
|
javascript_cache || build_javascript_cache
|
2022-10-19 01:20:10 +08:00
|
|
|
javascript_cache.update!(content: js_compiler.content, source_map: js_compiler.source_map)
|
2019-06-03 17:41:00 +08:00
|
|
|
else
|
|
|
|
javascript_cache&.destroy!
|
2019-05-24 22:25:55 +08:00
|
|
|
end
|
2017-04-12 22:52:52 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
after_destroy do
|
|
|
|
remove_from_cache!
|
2018-07-12 12:18:21 +08:00
|
|
|
Theme.clear_default! if SiteSetting.default_theme_id == self.id
|
2017-05-05 02:03:07 +08:00
|
|
|
|
|
|
|
if self.id
|
|
|
|
ColorScheme
|
|
|
|
.where(theme_id: self.id)
|
|
|
|
.where("id NOT IN (SELECT color_scheme_id FROM themes where color_scheme_id IS NOT NULL)")
|
|
|
|
.destroy_all
|
|
|
|
|
|
|
|
ColorScheme.where(theme_id: self.id).update_all(theme_id: nil)
|
|
|
|
end
|
2017-09-04 19:27:58 +08:00
|
|
|
|
|
|
|
Theme.expire_site_cache!
|
2017-04-12 22:52:52 +08:00
|
|
|
end
|
|
|
|
|
2020-05-29 20:04:51 +08:00
|
|
|
def self.compiler_version
|
|
|
|
get_set_cache "compiler_version" do
|
|
|
|
dependencies = [
|
|
|
|
BASE_COMPILER_VERSION,
|
2022-08-30 23:27:14 +08:00
|
|
|
EmberCli.ember_version,
|
2020-05-29 20:04:51 +08:00
|
|
|
GlobalSetting.cdn_url,
|
2022-08-11 18:03:57 +08:00
|
|
|
GlobalSetting.s3_cdn_url,
|
|
|
|
GlobalSetting.s3_endpoint,
|
|
|
|
GlobalSetting.s3_bucket,
|
2020-05-29 20:04:51 +08:00
|
|
|
Discourse.current_hostname,
|
|
|
|
]
|
|
|
|
Digest::SHA1.hexdigest(dependencies.join)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-08-08 12:46:34 +08:00
|
|
|
def self.get_set_cache(key, &blk)
|
2023-08-18 23:59:11 +08:00
|
|
|
cache.defer_get_set(key, &blk)
|
2018-08-08 12:46:34 +08:00
|
|
|
end
|
2017-04-12 22:52:52 +08:00
|
|
|
|
2018-07-12 12:18:21 +08:00
|
|
|
def self.theme_ids
|
2018-08-08 12:46:34 +08:00
|
|
|
get_set_cache "theme_ids" do
|
|
|
|
Theme.pluck(:id)
|
2017-04-15 01:35:12 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-06-15 14:57:17 +08:00
|
|
|
def self.parent_theme_ids
|
|
|
|
get_set_cache "parent_theme_ids" do
|
|
|
|
Theme.where(component: false).pluck(:id)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.is_parent_theme?(id)
|
|
|
|
self.parent_theme_ids.include?(id)
|
|
|
|
end
|
|
|
|
|
2018-07-12 12:18:21 +08:00
|
|
|
def self.user_theme_ids
|
2018-08-08 12:46:34 +08:00
|
|
|
get_set_cache "user_theme_ids" do
|
|
|
|
Theme.user_selectable.pluck(:id)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-04-10 03:54:44 +08:00
|
|
|
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
|
|
|
|
|
2021-10-29 23:46:52 +08:00
|
|
|
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
|
|
|
|
|
2018-08-08 12:46:34 +08:00
|
|
|
def self.components_for(theme_id)
|
|
|
|
get_set_cache "theme_components_for_#{theme_id}" do
|
2019-12-09 11:24:38 +08:00
|
|
|
ChildTheme.where(parent_theme_id: theme_id).pluck(:child_theme_id)
|
2017-04-15 01:35:12 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-04-12 22:52:52 +08:00
|
|
|
def self.expire_site_cache!
|
|
|
|
Site.clear_anon_cache!
|
2018-08-08 12:46:34 +08:00
|
|
|
clear_cache!
|
2017-04-12 22:52:52 +08:00
|
|
|
ApplicationSerializer.expire_cache_fragment!("user_themes")
|
2018-10-16 09:00:33 +08:00
|
|
|
ColorScheme.hex_cache.clear
|
2022-07-22 14:46:52 +08:00
|
|
|
CSP::Extension.clear_theme_extensions_cache!
|
|
|
|
SvgSprite.expire_cache
|
2017-04-12 22:52:52 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.clear_default!
|
2018-07-12 12:18:21 +08:00
|
|
|
SiteSetting.default_theme_id = -1
|
2017-04-12 22:52:52 +08:00
|
|
|
expire_site_cache!
|
|
|
|
end
|
|
|
|
|
2021-06-15 14:57:17 +08:00
|
|
|
def self.transform_ids(id)
|
|
|
|
return [] if id.blank?
|
2021-06-18 10:16:26 +08:00
|
|
|
id = id.to_i
|
2019-01-25 22:19:01 +08:00
|
|
|
|
2021-06-15 14:57:17 +08:00
|
|
|
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
|
2018-08-08 12:46:34 +08:00
|
|
|
|
2019-07-03 16:18:11 +08:00
|
|
|
disabled_ids =
|
|
|
|
Theme
|
|
|
|
.where(id: all_ids)
|
|
|
|
.includes(:remote_theme)
|
|
|
|
.select { |t| !t.supported? || !t.enabled? }
|
2021-06-15 14:57:17 +08:00
|
|
|
.map(&:id)
|
2018-08-08 12:46:34 +08:00
|
|
|
|
2019-01-26 01:00:19 +08:00
|
|
|
all_ids - disabled_ids
|
2019-01-25 22:19:01 +08:00
|
|
|
end
|
2018-08-08 12:46:34 +08:00
|
|
|
end
|
|
|
|
|
2017-04-12 22:52:52 +08:00
|
|
|
def set_default!
|
2018-08-24 09:30:00 +08:00
|
|
|
if component
|
2018-08-08 12:46:34 +08:00
|
|
|
raise Discourse::InvalidParameters.new(I18n.t("themes.errors.component_no_default"))
|
|
|
|
end
|
2018-07-12 12:18:21 +08:00
|
|
|
SiteSetting.default_theme_id = id
|
2017-04-12 22:52:52 +08:00
|
|
|
Theme.expire_site_cache!
|
|
|
|
end
|
|
|
|
|
2017-05-03 23:31:16 +08:00
|
|
|
def default?
|
2018-07-12 12:18:21 +08:00
|
|
|
SiteSetting.default_theme_id == id
|
2017-05-03 23:31:16 +08:00
|
|
|
end
|
|
|
|
|
2019-07-03 16:18:11 +08:00
|
|
|
def supported?
|
2019-01-25 22:19:01 +08:00
|
|
|
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
|
|
|
|
|
2018-08-24 09:30:00 +08:00
|
|
|
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
|
|
|
|
|
2023-06-14 01:07:47 +08:00
|
|
|
def validate_theme_fields
|
|
|
|
theme_fields.each do |field|
|
|
|
|
field.errors.full_messages.each { |message| errors.add(:base, message) } unless field.valid?
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-08-24 09:30:00 +08:00
|
|
|
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
|
2018-08-08 12:46:34 +08:00
|
|
|
end
|
2017-04-12 22:52:52 +08:00
|
|
|
|
2018-08-24 09:30:00 +08:00
|
|
|
def switch_to_theme!
|
|
|
|
return unless component
|
|
|
|
|
|
|
|
Theme.transaction do
|
2019-07-03 16:18:11 +08:00
|
|
|
self.enabled = true
|
2018-08-24 09:30:00 +08:00
|
|
|
self.component = false
|
|
|
|
ChildTheme.where("child_theme_id = ?", id).destroy_all
|
|
|
|
self.save!
|
2018-08-08 12:46:34 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-02-16 19:16:54 +08:00
|
|
|
def self.lookup_field(theme_id, target, field, skip_transformation: false, csp_nonce: nil)
|
2021-06-15 14:57:17 +08:00
|
|
|
return "" if theme_id.blank?
|
2018-08-08 12:46:34 +08:00
|
|
|
|
2021-06-15 14:57:17 +08:00
|
|
|
theme_ids = !skip_transformation ? transform_ids(theme_id) : [theme_id]
|
2024-02-16 19:16:54 +08:00
|
|
|
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
|
2017-04-12 22:52:52 +08:00
|
|
|
end
|
|
|
|
|
2020-03-11 21:30:45 +08:00
|
|
|
def self.lookup_modifier(theme_ids, modifier_name)
|
2021-06-18 10:16:26 +08:00
|
|
|
theme_ids = [theme_ids] unless theme_ids.is_a?(Array)
|
2021-06-15 14:57:17 +08:00
|
|
|
|
2020-05-29 20:04:51 +08:00
|
|
|
get_set_cache("#{theme_ids.join(",")}:modifier:#{modifier_name}:#{Theme.compiler_version}") do
|
2020-03-11 21:30:45 +08:00
|
|
|
ThemeModifierSet.resolve_modifier_for_themes(theme_ids, modifier_name)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-08-08 12:46:34 +08:00
|
|
|
def self.remove_from_cache!
|
2017-04-12 22:52:52 +08:00
|
|
|
clear_cache!
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.clear_cache!
|
2023-08-18 23:59:11 +08:00
|
|
|
cache.clear
|
2017-04-12 22:52:52 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.targets
|
2021-04-12 20:02:58 +08:00
|
|
|
@targets ||=
|
|
|
|
Enum.new(
|
|
|
|
common: 0,
|
|
|
|
desktop: 1,
|
|
|
|
mobile: 2,
|
|
|
|
settings: 3,
|
|
|
|
translations: 4,
|
|
|
|
extra_scss: 5,
|
|
|
|
extra_js: 6,
|
|
|
|
tests_js: 7,
|
FEATURE: Theme settings migrations (#24071)
This commit introduces a new feature that allows theme developers to manage the transformation of theme settings over time. Similar to Rails migrations, the theme settings migration system enables developers to write and execute migrations for theme settings, ensuring a smooth transition when changes are required in the format or structure of setting values.
Example use cases for the theme settings migration system:
1. Renaming a theme setting.
2. Changing the data type of a theme setting (e.g., transforming a string setting containing comma-separated values into a proper list setting).
3. Altering the format of data stored in a theme setting.
All of these use cases and more are now possible while preserving theme setting values for sites that have already modified their theme settings.
Usage:
1. Create a top-level directory called `migrations` in your theme/component, and then within the `migrations` directory create another directory called `settings`.
2. Inside the `migrations/settings` directory, create a JavaScript file using the format `XXXX-some-name.js`, where `XXXX` is a unique 4-digit number, and `some-name` is a descriptor of your choice that describes the migration.
3. Within the JavaScript file, define and export (as the default) a function called `migrate`. This function will receive a `Map` object and must also return a `Map` object (it's acceptable to return the same `Map` object that the function received).
4. The `Map` object received by the `migrate` function will include settings that have been overridden or changed by site administrators. Settings that have never been changed from the default will not be included.
5. The keys and values contained in the `Map` object that the `migrate` function returns will replace all the currently changed settings of the theme.
6. Migrations are executed in numerical order based on the XXXX segment in the migration filenames. For instance, `0001-some-migration.js` will be executed before `0002-another-migration.js`.
Here's a complete example migration script that renames a setting from `setting_with_old_name` to `setting_with_new_name`:
```js
// File name: 0001-rename-setting.js
export default function migrate(settings) {
if (settings.has("setting_with_old_name")) {
settings.set("setting_with_new_name", settings.get("setting_with_old_name"));
}
return settings;
}
```
Internal topic: t/109980
2023-11-02 13:10:15 +08:00
|
|
|
migrations: 8,
|
2021-04-12 20:02:58 +08:00
|
|
|
)
|
2018-03-05 08:04:23 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.lookup_target(target_id)
|
|
|
|
self.targets.invert[target_id]
|
2017-04-12 22:52:52 +08:00
|
|
|
end
|
|
|
|
|
2018-08-08 12:46:34 +08:00
|
|
|
def self.notify_theme_change(
|
|
|
|
theme_ids,
|
|
|
|
with_scheme: false,
|
|
|
|
clear_manager_cache: true,
|
|
|
|
all_themes: false
|
|
|
|
)
|
2017-04-12 22:52:52 +08:00
|
|
|
Stylesheet::Manager.clear_theme_cache!
|
2018-08-08 12:46:34 +08:00
|
|
|
targets = %i[mobile_theme desktop_theme]
|
2017-04-12 22:52:52 +08:00
|
|
|
|
2018-08-08 12:46:34 +08:00
|
|
|
if with_scheme
|
|
|
|
targets.prepend(:desktop, :mobile, :admin)
|
2019-09-16 21:56:19 +08:00
|
|
|
targets.append(*Discourse.find_plugin_css_assets(mobile_view: true, desktop_view: true))
|
2018-08-08 12:46:34 +08:00
|
|
|
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
|
2021-02-27 01:30:23 +08:00
|
|
|
message = refresh_message_for_targets(targets, theme_ids).flatten
|
2018-08-08 12:46:34 +08:00
|
|
|
end
|
2017-04-12 22:52:52 +08:00
|
|
|
|
|
|
|
MessageBus.publish("/file-change", message)
|
|
|
|
end
|
|
|
|
|
2018-08-08 12:46:34 +08:00
|
|
|
def notify_theme_change(with_scheme: false)
|
2020-05-01 23:44:50 +08:00
|
|
|
DB.after_commit do
|
2021-06-15 14:57:17 +08:00
|
|
|
theme_ids = Theme.transform_ids(id)
|
2020-05-01 23:44:50 +08:00
|
|
|
self.class.notify_theme_change(theme_ids, with_scheme: with_scheme)
|
|
|
|
end
|
2018-08-08 12:46:34 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.refresh_message_for_targets(targets, theme_ids)
|
2021-06-18 10:16:26 +08:00
|
|
|
theme_ids = [theme_ids] unless theme_ids.is_a?(Array)
|
2021-06-15 14:57:17 +08:00
|
|
|
|
|
|
|
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
|
2017-04-12 22:52:52 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-08-08 12:46:34 +08:00
|
|
|
def self.resolve_baked_field(theme_ids, target, name)
|
2023-09-01 03:12:03 +08:00
|
|
|
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")
|
2024-02-16 19:16:54 +08:00
|
|
|
<script defer src="#{c.url}" data-theme-id="#{c.theme_id}" nonce="#{ThemeField::CSP_NONCE_PLACEHOLDER}"></script>
|
2022-12-01 02:43:01 +08:00
|
|
|
HTML
|
2023-09-01 03:12:03 +08:00
|
|
|
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
|
|
|
|
|
|
|
|
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)
|
2019-06-03 17:41:00 +08:00
|
|
|
end
|
2018-08-08 12:46:34 +08:00
|
|
|
end
|
2017-04-12 22:52:52 +08:00
|
|
|
|
2018-08-08 12:46:34 +08:00
|
|
|
def self.list_baked_fields(theme_ids, target, name)
|
|
|
|
target = target.to_sym
|
2019-04-12 18:36:08 +08:00
|
|
|
name = name&.to_sym
|
2017-04-12 22:52:52 +08:00
|
|
|
|
2019-01-17 19:46:11 +08:00
|
|
|
if target == :translations
|
|
|
|
fields = ThemeField.find_first_locale_fields(theme_ids, I18n.fallbacks[name])
|
|
|
|
else
|
2021-03-02 22:20:43 +08:00
|
|
|
target = :mobile if target == :mobile_theme
|
|
|
|
target = :desktop if target == :desktop_theme
|
2019-01-17 19:46:11 +08:00
|
|
|
fields =
|
|
|
|
ThemeField.find_by_theme_ids(theme_ids).where(
|
|
|
|
target_id: [Theme.targets[target], Theme.targets[:common]],
|
|
|
|
)
|
2019-04-12 18:36:08 +08:00
|
|
|
fields = fields.where(name: name.to_s) unless name.nil?
|
|
|
|
fields = fields.order(:target_id)
|
2019-01-17 19:46:11 +08:00
|
|
|
end
|
2017-04-12 22:52:52 +08:00
|
|
|
|
2018-08-08 12:46:34 +08:00
|
|
|
fields.each(&:ensure_baked!)
|
|
|
|
fields
|
2017-04-12 22:52:52 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def resolve_baked_field(target, name)
|
|
|
|
list_baked_fields(target, name).map { |f| f.value_baked || f.value }.join("\n")
|
|
|
|
end
|
|
|
|
|
|
|
|
def list_baked_fields(target, name)
|
2021-06-15 14:57:17 +08:00
|
|
|
theme_ids = Theme.transform_ids(id)
|
|
|
|
theme_ids = [theme_ids.first] if name != :color_definitions
|
2018-08-08 12:46:34 +08:00
|
|
|
self.class.list_baked_fields(theme_ids, target, name)
|
2017-04-12 22:52:52 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def remove_from_cache!
|
|
|
|
self.class.remove_from_cache!
|
|
|
|
end
|
|
|
|
|
|
|
|
def changed_fields
|
|
|
|
@changed_fields ||= []
|
|
|
|
end
|
|
|
|
|
2017-04-18 03:56:13 +08:00
|
|
|
def changed_colors
|
|
|
|
@changed_colors ||= []
|
|
|
|
end
|
|
|
|
|
2019-08-12 18:02:38 +08:00
|
|
|
def changed_schemes
|
|
|
|
@changed_schemes ||= Set.new
|
|
|
|
end
|
|
|
|
|
2017-05-08 23:38:48 +08:00
|
|
|
def set_field(target:, name:, value: nil, type: nil, type_id: nil, upload_id: nil)
|
2017-04-12 22:52:52 +08:00
|
|
|
name = name.to_s
|
|
|
|
|
|
|
|
target_id = Theme.targets[target.to_sym]
|
|
|
|
raise "Unknown target #{target} passed to set field" unless target_id
|
|
|
|
|
2019-01-17 19:46:11 +08:00
|
|
|
type_id ||=
|
|
|
|
type ? ThemeField.types[type.to_sym] : ThemeField.guess_type(name: name, target: target)
|
2017-05-03 04:01:01 +08:00
|
|
|
raise "Unknown type #{type} passed to set field" unless type_id
|
|
|
|
|
2017-05-10 05:20:28 +08:00
|
|
|
value ||= ""
|
2017-05-08 23:38:48 +08:00
|
|
|
|
2023-04-28 02:04:58 +08:00
|
|
|
field = theme_fields.find_by(name: name, target_id: target_id, type_id: type_id)
|
|
|
|
|
2017-04-12 22:52:52 +08:00
|
|
|
if field
|
2017-05-10 05:20:28 +08:00
|
|
|
if value.blank? && !upload_id
|
2023-04-28 02:04:58 +08:00
|
|
|
field.destroy
|
2017-04-12 22:52:52 +08:00
|
|
|
else
|
2017-05-10 05:20:28 +08:00
|
|
|
if field.value != value || field.upload_id != upload_id
|
2017-04-12 22:52:52 +08:00
|
|
|
field.value = value
|
2017-05-10 05:20:28 +08:00
|
|
|
field.upload_id = upload_id
|
2017-04-12 22:52:52 +08:00
|
|
|
changed_fields << field
|
|
|
|
end
|
|
|
|
end
|
|
|
|
else
|
2017-05-08 23:38:48 +08:00
|
|
|
if value.present? || upload_id.present?
|
2023-08-04 18:02:26 +08:00
|
|
|
field =
|
|
|
|
theme_fields.build(
|
|
|
|
target_id: target_id,
|
|
|
|
value: value,
|
|
|
|
name: name,
|
|
|
|
type_id: type_id,
|
|
|
|
upload_id: upload_id,
|
|
|
|
)
|
|
|
|
changed_fields << field
|
2023-01-09 20:20:10 +08:00
|
|
|
end
|
2017-04-12 22:52:52 +08:00
|
|
|
end
|
2023-08-04 18:02:26 +08:00
|
|
|
field
|
2017-04-12 22:52:52 +08:00
|
|
|
end
|
|
|
|
|
2023-06-23 02:57:39 +08:00
|
|
|
def child_theme_ids=(theme_ids)
|
|
|
|
super(theme_ids)
|
2023-07-12 22:49:28 +08:00
|
|
|
Theme.clear_cache!
|
2023-06-23 02:57:39 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def parent_theme_ids=(theme_ids)
|
|
|
|
super(theme_ids)
|
2023-07-12 22:49:28 +08:00
|
|
|
Theme.clear_cache!
|
2023-06-23 02:57:39 +08:00
|
|
|
end
|
|
|
|
|
2019-11-28 13:19:01 +08:00
|
|
|
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
|
2018-08-08 12:46:34 +08:00
|
|
|
if new_relation.save
|
|
|
|
child_themes.reload
|
2019-11-28 13:19:01 +08:00
|
|
|
parent_themes.reload
|
2018-08-08 12:46:34 +08:00
|
|
|
save!
|
2019-01-25 22:19:01 +08:00
|
|
|
Theme.clear_cache!
|
2018-08-08 12:46:34 +08:00
|
|
|
else
|
|
|
|
raise Discourse::InvalidParameters.new(new_relation.errors.full_messages.join(", "))
|
|
|
|
end
|
2017-04-12 22:52:52 +08:00
|
|
|
end
|
2018-03-05 08:04:23 +08:00
|
|
|
|
2019-01-25 22:19:01 +08:00
|
|
|
def internal_translations
|
2019-05-31 21:49:59 +08:00
|
|
|
@internal_translations ||= translations(internal: true)
|
2019-01-25 22:19:01 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def translations(internal: false)
|
2019-01-17 19:46:11 +08:00
|
|
|
fallbacks = I18n.fallbacks[I18n.locale]
|
|
|
|
begin
|
2019-02-26 22:22:02 +08:00
|
|
|
data =
|
|
|
|
locale_fields.first&.translation_data(
|
|
|
|
with_overrides: false,
|
|
|
|
internal: internal,
|
|
|
|
fallback_fields: locale_fields,
|
|
|
|
)
|
2019-01-17 19:46:11 +08:00
|
|
|
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
|
|
|
|
|
2018-03-05 08:04:23 +08:00
|
|
|
def settings
|
2018-12-21 01:13:05 +08:00
|
|
|
field = settings_field
|
2024-02-01 10:26:56 +08:00
|
|
|
settings = {}
|
2024-01-11 14:04:02 +08:00
|
|
|
|
2024-02-01 10:26:56 +08:00
|
|
|
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
|
2024-01-11 14:04:02 +08:00
|
|
|
|
2018-03-05 08:04:23 +08:00
|
|
|
settings
|
|
|
|
end
|
|
|
|
|
|
|
|
def cached_settings
|
2022-02-19 06:30:20 +08:00
|
|
|
Theme.get_set_cache "settings_for_theme_#{self.id}" do
|
2020-05-02 00:51:11 +08:00
|
|
|
build_settings_hash
|
|
|
|
end
|
|
|
|
end
|
2019-08-21 14:50:47 +08:00
|
|
|
|
2021-04-29 04:12:08 +08:00
|
|
|
def cached_default_settings
|
2022-02-19 06:30:20 +08:00
|
|
|
Theme.get_set_cache "default_settings_for_theme_#{self.id}" do
|
2021-04-29 04:12:08 +08:00
|
|
|
settings_hash = {}
|
2024-02-01 10:26:56 +08:00
|
|
|
self.settings.each { |name, setting| settings_hash[name] = setting.default }
|
2022-10-20 13:00:29 +08:00
|
|
|
|
|
|
|
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?
|
|
|
|
|
2021-04-29 04:12:08 +08:00
|
|
|
settings_hash
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-05-02 00:51:11 +08:00
|
|
|
def build_settings_hash
|
|
|
|
hash = {}
|
2024-02-01 10:26:56 +08:00
|
|
|
self.settings.each { |name, setting| hash[name] = setting.value }
|
2019-08-21 14:50:47 +08:00
|
|
|
|
2022-10-20 13:00:29 +08:00
|
|
|
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?
|
2022-04-07 05:58:10 +08:00
|
|
|
|
2022-10-20 13:00:29 +08:00
|
|
|
hash
|
|
|
|
end
|
|
|
|
|
|
|
|
def build_theme_uploads_hash
|
|
|
|
hash = {}
|
2024-07-02 07:55:06 +08:00
|
|
|
upload_fields
|
|
|
|
.includes(:javascript_cache, :upload)
|
|
|
|
.each do |field|
|
|
|
|
hash[field.name] = Discourse.store.cdn_url(field.upload.url) if field.upload&.url
|
|
|
|
end
|
2022-10-20 13:00:29 +08:00
|
|
|
hash
|
|
|
|
end
|
|
|
|
|
|
|
|
def build_local_theme_uploads_hash
|
|
|
|
hash = {}
|
2024-07-02 07:55:06 +08:00
|
|
|
upload_fields
|
|
|
|
.includes(:javascript_cache, :upload)
|
|
|
|
.each do |field|
|
|
|
|
hash[field.name] = field.javascript_cache.local_url if field.javascript_cache
|
|
|
|
end
|
2020-05-02 00:51:11 +08:00
|
|
|
hash
|
2018-03-05 08:04:23 +08:00
|
|
|
end
|
|
|
|
|
2023-10-23 07:41:40 +08:00
|
|
|
# Retrieves a theme setting
|
|
|
|
#
|
|
|
|
# @param setting_name [String, Symbol] The name of the setting to retrieve.
|
|
|
|
#
|
|
|
|
# @return [Object] The value of the setting that matches the provided name.
|
|
|
|
#
|
|
|
|
# @raise [Discourse::NotFound] If no setting is found with the provided name.
|
|
|
|
#
|
|
|
|
# @example
|
|
|
|
# theme.get_setting("some_boolean") => True
|
|
|
|
# theme.get_setting("some_string") => "hello"
|
|
|
|
# theme.get_setting(:some_boolean) => True
|
|
|
|
# theme.get_setting(:some_string) => "hello"
|
|
|
|
#
|
|
|
|
def get_setting(setting_name)
|
2024-02-01 10:26:56 +08:00
|
|
|
target_setting = settings[setting_name.to_sym]
|
2023-10-23 07:41:40 +08:00
|
|
|
raise Discourse::NotFound unless target_setting
|
|
|
|
target_setting.value
|
|
|
|
end
|
|
|
|
|
2018-03-05 08:04:23 +08:00
|
|
|
def update_setting(setting_name, new_value)
|
2024-02-01 10:26:56 +08:00
|
|
|
target_setting = settings[setting_name.to_sym]
|
2018-03-05 08:04:23 +08:00
|
|
|
raise Discourse::NotFound unless target_setting
|
|
|
|
target_setting.value = new_value
|
2021-11-22 20:16:56 +08:00
|
|
|
self.theme_setting_requests_refresh = true if target_setting.requests_refresh?
|
2018-03-05 08:04:23 +08:00
|
|
|
end
|
2019-01-17 19:46:11 +08:00
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
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
|
2019-01-23 22:40:21 +08:00
|
|
|
|
|
|
|
def generate_metadata_hash
|
2019-01-25 22:19:01 +08:00
|
|
|
{}.tap do |meta|
|
|
|
|
meta[:name] = name
|
|
|
|
meta[:component] = component
|
|
|
|
|
|
|
|
RemoteTheme::METADATA_PROPERTIES.each do |property|
|
|
|
|
meta[property] = remote_theme&.public_send(property)
|
2019-01-28 19:55:58 +08:00
|
|
|
meta[property] = nil if meta[property] == "URL" # Clean up old discourse_theme CLI placeholders
|
2019-01-25 22:19:01 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
meta[:assets] = {}.tap do |hash|
|
2019-01-23 22:40:21 +08:00
|
|
|
theme_fields
|
|
|
|
.where(type_id: ThemeField.types[:theme_upload_var])
|
2019-02-27 17:45:22 +08:00
|
|
|
.each { |field| hash[field.name] = field.file_path }
|
2019-01-25 22:19:01 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
meta[:color_schemes] = {}.tap do |hash|
|
2019-01-23 22:40:21 +08:00
|
|
|
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 }
|
2023-01-09 20:20:10 +08:00
|
|
|
end
|
2019-01-23 22:40:21 +08:00
|
|
|
end
|
|
|
|
end
|
2019-01-25 22:19:01 +08:00
|
|
|
|
2020-03-11 21:30:45 +08:00
|
|
|
meta[:modifiers] = {}.tap do |hash|
|
2020-03-13 00:35:28 +08:00
|
|
|
ThemeModifierSet.modifiers.keys.each do |modifier|
|
2020-03-11 21:30:45 +08:00
|
|
|
value = self.theme_modifier_set.public_send(modifier)
|
|
|
|
hash[modifier] = value if !value.nil?
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-01-29 00:00:33 +08:00
|
|
|
meta[
|
|
|
|
:learn_more
|
|
|
|
] = "https://meta.discourse.org/t/beginners-guide-to-using-discourse-themes/91966"
|
2019-01-25 22:19:01 +08:00
|
|
|
end
|
2019-01-23 22:40:21 +08:00
|
|
|
end
|
2019-07-03 16:18:11 +08:00
|
|
|
|
|
|
|
def disabled_by
|
|
|
|
find_disable_action_log&.acting_user
|
|
|
|
end
|
|
|
|
|
|
|
|
def disabled_at
|
|
|
|
find_disable_action_log&.created_at
|
|
|
|
end
|
|
|
|
|
2021-04-27 21:33:43 +08:00
|
|
|
def with_scss_load_paths
|
|
|
|
return yield([]) if self.extra_scss_fields.empty?
|
2021-02-03 02:09:41 +08:00
|
|
|
|
2021-04-27 21:33:43 +08:00
|
|
|
ThemeStore::ZipExporter
|
|
|
|
.new(self)
|
|
|
|
.with_export_dir(extra_scss_only: true) { |dir| yield ["#{dir}/stylesheets"] }
|
2021-02-03 02:09:41 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def scss_variables
|
2022-02-21 19:15:35 +08:00
|
|
|
settings_hash = build_settings_hash
|
|
|
|
theme_variable_fields = var_theme_fields
|
|
|
|
|
|
|
|
return if theme_variable_fields.empty? && settings_hash.empty?
|
2021-02-03 02:09:41 +08:00
|
|
|
|
|
|
|
contents = +""
|
|
|
|
|
2022-02-21 19:15:35 +08:00
|
|
|
theme_variable_fields&.each do |field|
|
2021-02-03 02:09:41 +08:00
|
|
|
if field.type_id == ThemeField.types[:theme_upload_var]
|
|
|
|
if upload = field.upload
|
|
|
|
url = upload_cdn_path(upload.url)
|
|
|
|
contents << "$#{field.name}: unquote(\"#{url}\");"
|
2024-07-02 07:55:06 +08:00
|
|
|
else
|
|
|
|
contents << "$#{field.name}: unquote(\"\");"
|
2021-02-03 02:09:41 +08:00
|
|
|
end
|
|
|
|
else
|
|
|
|
contents << to_scss_variable(field.name, field.value)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-02-21 19:15:35 +08:00
|
|
|
settings_hash&.each do |name, value|
|
2022-04-07 05:58:10 +08:00
|
|
|
next if name == "theme_uploads" || name == "theme_uploads_local"
|
2021-02-03 02:09:41 +08:00
|
|
|
contents << to_scss_variable(name, value)
|
|
|
|
end
|
|
|
|
|
|
|
|
contents
|
|
|
|
end
|
|
|
|
|
DEV: Introduce `run_theme_migration` spec helper in test environment (#26845)
This commit introduces the `run_theme_migration` spec helper to allow
theme developers to write RSpec tests for theme migrations. For example,
this allows the following RSpec test to be written in themes:
```
RSpec.describe "0003-migrate-small-links-setting migration" do
let!(:theme) { upload_theme_component }
it "should set target property to `_blank` if previous target component is not valid or empty" do
theme.theme_settings.create!(
name: "small_links",
theme: theme,
data_type: ThemeSetting.types[:string],
value: "some text, #|some text 2, #, invalid target",
)
run_theme_migration(theme, "0003-migrate-small-links-setting")
expect(theme.settings[:small_links].value).to eq(
[
{ "text" => "some text", "url" => "#", "target" => "_blank" },
{ "text" => "some text 2", "url" => "#", "target" => "_blank" },
],
)
end
end
```
This change is being introduced because we realised that writting just
javascript tests for the migrations is insufficient since javascript
tests do not ensure that the migrated theme settings can actually be
successfully saved into the database. Hence, we are introduce this
helper as a way for theme developers to write "end-to-end" migrations
tests.
2024-05-03 06:29:18 +08:00
|
|
|
def migrate_settings(start_transaction: true, fields: nil, allow_out_of_sequence_migration: false)
|
2024-08-19 20:44:17 +08:00
|
|
|
block = ->(*) do
|
FEATURE: Theme settings migrations (#24071)
This commit introduces a new feature that allows theme developers to manage the transformation of theme settings over time. Similar to Rails migrations, the theme settings migration system enables developers to write and execute migrations for theme settings, ensuring a smooth transition when changes are required in the format or structure of setting values.
Example use cases for the theme settings migration system:
1. Renaming a theme setting.
2. Changing the data type of a theme setting (e.g., transforming a string setting containing comma-separated values into a proper list setting).
3. Altering the format of data stored in a theme setting.
All of these use cases and more are now possible while preserving theme setting values for sites that have already modified their theme settings.
Usage:
1. Create a top-level directory called `migrations` in your theme/component, and then within the `migrations` directory create another directory called `settings`.
2. Inside the `migrations/settings` directory, create a JavaScript file using the format `XXXX-some-name.js`, where `XXXX` is a unique 4-digit number, and `some-name` is a descriptor of your choice that describes the migration.
3. Within the JavaScript file, define and export (as the default) a function called `migrate`. This function will receive a `Map` object and must also return a `Map` object (it's acceptable to return the same `Map` object that the function received).
4. The `Map` object received by the `migrate` function will include settings that have been overridden or changed by site administrators. Settings that have never been changed from the default will not be included.
5. The keys and values contained in the `Map` object that the `migrate` function returns will replace all the currently changed settings of the theme.
6. Migrations are executed in numerical order based on the XXXX segment in the migration filenames. For instance, `0001-some-migration.js` will be executed before `0002-another-migration.js`.
Here's a complete example migration script that renames a setting from `setting_with_old_name` to `setting_with_new_name`:
```js
// File name: 0001-rename-setting.js
export default function migrate(settings) {
if (settings.has("setting_with_old_name")) {
settings.set("setting_with_new_name", settings.get("setting_with_old_name"));
}
return settings;
}
```
Internal topic: t/109980
2023-11-02 13:10:15 +08:00
|
|
|
runner = ThemeSettingsMigrationsRunner.new(self)
|
DEV: Introduce `run_theme_migration` spec helper in test environment (#26845)
This commit introduces the `run_theme_migration` spec helper to allow
theme developers to write RSpec tests for theme migrations. For example,
this allows the following RSpec test to be written in themes:
```
RSpec.describe "0003-migrate-small-links-setting migration" do
let!(:theme) { upload_theme_component }
it "should set target property to `_blank` if previous target component is not valid or empty" do
theme.theme_settings.create!(
name: "small_links",
theme: theme,
data_type: ThemeSetting.types[:string],
value: "some text, #|some text 2, #, invalid target",
)
run_theme_migration(theme, "0003-migrate-small-links-setting")
expect(theme.settings[:small_links].value).to eq(
[
{ "text" => "some text", "url" => "#", "target" => "_blank" },
{ "text" => "some text 2", "url" => "#", "target" => "_blank" },
],
)
end
end
```
This change is being introduced because we realised that writting just
javascript tests for the migrations is insufficient since javascript
tests do not ensure that the migrated theme settings can actually be
successfully saved into the database. Hence, we are introduce this
helper as a way for theme developers to write "end-to-end" migrations
tests.
2024-05-03 06:29:18 +08:00
|
|
|
results =
|
|
|
|
runner.run(fields:, raise_error_on_out_of_sequence: !allow_out_of_sequence_migration)
|
FEATURE: Theme settings migrations (#24071)
This commit introduces a new feature that allows theme developers to manage the transformation of theme settings over time. Similar to Rails migrations, the theme settings migration system enables developers to write and execute migrations for theme settings, ensuring a smooth transition when changes are required in the format or structure of setting values.
Example use cases for the theme settings migration system:
1. Renaming a theme setting.
2. Changing the data type of a theme setting (e.g., transforming a string setting containing comma-separated values into a proper list setting).
3. Altering the format of data stored in a theme setting.
All of these use cases and more are now possible while preserving theme setting values for sites that have already modified their theme settings.
Usage:
1. Create a top-level directory called `migrations` in your theme/component, and then within the `migrations` directory create another directory called `settings`.
2. Inside the `migrations/settings` directory, create a JavaScript file using the format `XXXX-some-name.js`, where `XXXX` is a unique 4-digit number, and `some-name` is a descriptor of your choice that describes the migration.
3. Within the JavaScript file, define and export (as the default) a function called `migrate`. This function will receive a `Map` object and must also return a `Map` object (it's acceptable to return the same `Map` object that the function received).
4. The `Map` object received by the `migrate` function will include settings that have been overridden or changed by site administrators. Settings that have never been changed from the default will not be included.
5. The keys and values contained in the `Map` object that the `migrate` function returns will replace all the currently changed settings of the theme.
6. Migrations are executed in numerical order based on the XXXX segment in the migration filenames. For instance, `0001-some-migration.js` will be executed before `0002-another-migration.js`.
Here's a complete example migration script that renames a setting from `setting_with_old_name` to `setting_with_new_name`:
```js
// File name: 0001-rename-setting.js
export default function migrate(settings) {
if (settings.has("setting_with_old_name")) {
settings.set("setting_with_new_name", settings.get("setting_with_old_name"));
}
return settings;
}
```
Internal topic: t/109980
2023-11-02 13:10:15 +08:00
|
|
|
|
|
|
|
next if results.blank?
|
2021-03-11 09:15:04 +08:00
|
|
|
|
FEATURE: Theme settings migrations (#24071)
This commit introduces a new feature that allows theme developers to manage the transformation of theme settings over time. Similar to Rails migrations, the theme settings migration system enables developers to write and execute migrations for theme settings, ensuring a smooth transition when changes are required in the format or structure of setting values.
Example use cases for the theme settings migration system:
1. Renaming a theme setting.
2. Changing the data type of a theme setting (e.g., transforming a string setting containing comma-separated values into a proper list setting).
3. Altering the format of data stored in a theme setting.
All of these use cases and more are now possible while preserving theme setting values for sites that have already modified their theme settings.
Usage:
1. Create a top-level directory called `migrations` in your theme/component, and then within the `migrations` directory create another directory called `settings`.
2. Inside the `migrations/settings` directory, create a JavaScript file using the format `XXXX-some-name.js`, where `XXXX` is a unique 4-digit number, and `some-name` is a descriptor of your choice that describes the migration.
3. Within the JavaScript file, define and export (as the default) a function called `migrate`. This function will receive a `Map` object and must also return a `Map` object (it's acceptable to return the same `Map` object that the function received).
4. The `Map` object received by the `migrate` function will include settings that have been overridden or changed by site administrators. Settings that have never been changed from the default will not be included.
5. The keys and values contained in the `Map` object that the `migrate` function returns will replace all the currently changed settings of the theme.
6. Migrations are executed in numerical order based on the XXXX segment in the migration filenames. For instance, `0001-some-migration.js` will be executed before `0002-another-migration.js`.
Here's a complete example migration script that renames a setting from `setting_with_old_name` to `setting_with_new_name`:
```js
// File name: 0001-rename-setting.js
export default function migrate(settings) {
if (settings.has("setting_with_old_name")) {
settings.set("setting_with_new_name", settings.get("setting_with_old_name"));
}
return settings;
}
```
Internal topic: t/109980
2023-11-02 13:10:15 +08:00
|
|
|
old_settings = self.theme_settings.pluck(:name)
|
|
|
|
self.theme_settings.destroy_all
|
|
|
|
|
|
|
|
final_result = results.last
|
2024-01-11 14:04:02 +08:00
|
|
|
|
FEATURE: Theme settings migrations (#24071)
This commit introduces a new feature that allows theme developers to manage the transformation of theme settings over time. Similar to Rails migrations, the theme settings migration system enables developers to write and execute migrations for theme settings, ensuring a smooth transition when changes are required in the format or structure of setting values.
Example use cases for the theme settings migration system:
1. Renaming a theme setting.
2. Changing the data type of a theme setting (e.g., transforming a string setting containing comma-separated values into a proper list setting).
3. Altering the format of data stored in a theme setting.
All of these use cases and more are now possible while preserving theme setting values for sites that have already modified their theme settings.
Usage:
1. Create a top-level directory called `migrations` in your theme/component, and then within the `migrations` directory create another directory called `settings`.
2. Inside the `migrations/settings` directory, create a JavaScript file using the format `XXXX-some-name.js`, where `XXXX` is a unique 4-digit number, and `some-name` is a descriptor of your choice that describes the migration.
3. Within the JavaScript file, define and export (as the default) a function called `migrate`. This function will receive a `Map` object and must also return a `Map` object (it's acceptable to return the same `Map` object that the function received).
4. The `Map` object received by the `migrate` function will include settings that have been overridden or changed by site administrators. Settings that have never been changed from the default will not be included.
5. The keys and values contained in the `Map` object that the `migrate` function returns will replace all the currently changed settings of the theme.
6. Migrations are executed in numerical order based on the XXXX segment in the migration filenames. For instance, `0001-some-migration.js` will be executed before `0002-another-migration.js`.
Here's a complete example migration script that renames a setting from `setting_with_old_name` to `setting_with_new_name`:
```js
// File name: 0001-rename-setting.js
export default function migrate(settings) {
if (settings.has("setting_with_old_name")) {
settings.set("setting_with_new_name", settings.get("setting_with_old_name"));
}
return settings;
}
```
Internal topic: t/109980
2023-11-02 13:10:15 +08:00
|
|
|
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)
|
2021-03-11 09:15:04 +08:00
|
|
|
else
|
FEATURE: Theme settings migrations (#24071)
This commit introduces a new feature that allows theme developers to manage the transformation of theme settings over time. Similar to Rails migrations, the theme settings migration system enables developers to write and execute migrations for theme settings, ensuring a smooth transition when changes are required in the format or structure of setting values.
Example use cases for the theme settings migration system:
1. Renaming a theme setting.
2. Changing the data type of a theme setting (e.g., transforming a string setting containing comma-separated values into a proper list setting).
3. Altering the format of data stored in a theme setting.
All of these use cases and more are now possible while preserving theme setting values for sites that have already modified their theme settings.
Usage:
1. Create a top-level directory called `migrations` in your theme/component, and then within the `migrations` directory create another directory called `settings`.
2. Inside the `migrations/settings` directory, create a JavaScript file using the format `XXXX-some-name.js`, where `XXXX` is a unique 4-digit number, and `some-name` is a descriptor of your choice that describes the migration.
3. Within the JavaScript file, define and export (as the default) a function called `migrate`. This function will receive a `Map` object and must also return a `Map` object (it's acceptable to return the same `Map` object that the function received).
4. The `Map` object received by the `migrate` function will include settings that have been overridden or changed by site administrators. Settings that have never been changed from the default will not be included.
5. The keys and values contained in the `Map` object that the `migrate` function returns will replace all the currently changed settings of the theme.
6. Migrations are executed in numerical order based on the XXXX segment in the migration filenames. For instance, `0001-some-migration.js` will be executed before `0002-another-migration.js`.
Here's a complete example migration script that renames a setting from `setting_with_old_name` to `setting_with_new_name`:
```js
// File name: 0001-rename-setting.js
export default function migrate(settings) {
if (settings.has("setting_with_old_name")) {
settings.set("setting_with_new_name", settings.get("setting_with_old_name"));
}
return settings;
}
```
Internal topic: t/109980
2023-11-02 13:10:15 +08:00
|
|
|
raise Theme::SettingsMigrationError.new(
|
|
|
|
I18n.t(
|
|
|
|
"themes.import_error.migrations.unknown_setting_returned_by_migration",
|
|
|
|
name: final_result[:original_name],
|
|
|
|
setting_name: key,
|
|
|
|
),
|
|
|
|
)
|
2021-03-11 09:15:04 +08:00
|
|
|
end
|
|
|
|
end
|
FEATURE: Theme settings migrations (#24071)
This commit introduces a new feature that allows theme developers to manage the transformation of theme settings over time. Similar to Rails migrations, the theme settings migration system enables developers to write and execute migrations for theme settings, ensuring a smooth transition when changes are required in the format or structure of setting values.
Example use cases for the theme settings migration system:
1. Renaming a theme setting.
2. Changing the data type of a theme setting (e.g., transforming a string setting containing comma-separated values into a proper list setting).
3. Altering the format of data stored in a theme setting.
All of these use cases and more are now possible while preserving theme setting values for sites that have already modified their theme settings.
Usage:
1. Create a top-level directory called `migrations` in your theme/component, and then within the `migrations` directory create another directory called `settings`.
2. Inside the `migrations/settings` directory, create a JavaScript file using the format `XXXX-some-name.js`, where `XXXX` is a unique 4-digit number, and `some-name` is a descriptor of your choice that describes the migration.
3. Within the JavaScript file, define and export (as the default) a function called `migrate`. This function will receive a `Map` object and must also return a `Map` object (it's acceptable to return the same `Map` object that the function received).
4. The `Map` object received by the `migrate` function will include settings that have been overridden or changed by site administrators. Settings that have never been changed from the default will not be included.
5. The keys and values contained in the `Map` object that the `migrate` function returns will replace all the currently changed settings of the theme.
6. Migrations are executed in numerical order based on the XXXX segment in the migration filenames. For instance, `0001-some-migration.js` will be executed before `0002-another-migration.js`.
Here's a complete example migration script that renames a setting from `setting_with_old_name` to `setting_with_new_name`:
```js
// File name: 0001-rename-setting.js
export default function migrate(settings) {
if (settings.has("setting_with_old_name")) {
settings.set("setting_with_new_name", settings.get("setting_with_old_name"));
}
return settings;
}
```
Internal topic: t/109980
2023-11-02 13:10:15 +08:00
|
|
|
|
|
|
|
results.each do |res|
|
|
|
|
record =
|
|
|
|
ThemeSettingsMigration.new(
|
|
|
|
theme_id: self.id,
|
|
|
|
version: res[:version],
|
|
|
|
name: res[:name],
|
|
|
|
theme_field_id: res[:theme_field_id],
|
|
|
|
)
|
DEV: Introduce `run_theme_migration` spec helper in test environment (#26845)
This commit introduces the `run_theme_migration` spec helper to allow
theme developers to write RSpec tests for theme migrations. For example,
this allows the following RSpec test to be written in themes:
```
RSpec.describe "0003-migrate-small-links-setting migration" do
let!(:theme) { upload_theme_component }
it "should set target property to `_blank` if previous target component is not valid or empty" do
theme.theme_settings.create!(
name: "small_links",
theme: theme,
data_type: ThemeSetting.types[:string],
value: "some text, #|some text 2, #, invalid target",
)
run_theme_migration(theme, "0003-migrate-small-links-setting")
expect(theme.settings[:small_links].value).to eq(
[
{ "text" => "some text", "url" => "#", "target" => "_blank" },
{ "text" => "some text 2", "url" => "#", "target" => "_blank" },
],
)
end
end
```
This change is being introduced because we realised that writting just
javascript tests for the migrations is insufficient since javascript
tests do not ensure that the migrated theme settings can actually be
successfully saved into the database. Hence, we are introduce this
helper as a way for theme developers to write "end-to-end" migrations
tests.
2024-05-03 06:29:18 +08:00
|
|
|
|
FEATURE: Theme settings migrations (#24071)
This commit introduces a new feature that allows theme developers to manage the transformation of theme settings over time. Similar to Rails migrations, the theme settings migration system enables developers to write and execute migrations for theme settings, ensuring a smooth transition when changes are required in the format or structure of setting values.
Example use cases for the theme settings migration system:
1. Renaming a theme setting.
2. Changing the data type of a theme setting (e.g., transforming a string setting containing comma-separated values into a proper list setting).
3. Altering the format of data stored in a theme setting.
All of these use cases and more are now possible while preserving theme setting values for sites that have already modified their theme settings.
Usage:
1. Create a top-level directory called `migrations` in your theme/component, and then within the `migrations` directory create another directory called `settings`.
2. Inside the `migrations/settings` directory, create a JavaScript file using the format `XXXX-some-name.js`, where `XXXX` is a unique 4-digit number, and `some-name` is a descriptor of your choice that describes the migration.
3. Within the JavaScript file, define and export (as the default) a function called `migrate`. This function will receive a `Map` object and must also return a `Map` object (it's acceptable to return the same `Map` object that the function received).
4. The `Map` object received by the `migrate` function will include settings that have been overridden or changed by site administrators. Settings that have never been changed from the default will not be included.
5. The keys and values contained in the `Map` object that the `migrate` function returns will replace all the currently changed settings of the theme.
6. Migrations are executed in numerical order based on the XXXX segment in the migration filenames. For instance, `0001-some-migration.js` will be executed before `0002-another-migration.js`.
Here's a complete example migration script that renames a setting from `setting_with_old_name` to `setting_with_new_name`:
```js
// File name: 0001-rename-setting.js
export default function migrate(settings) {
if (settings.has("setting_with_old_name")) {
settings.set("setting_with_new_name", settings.get("setting_with_old_name"));
}
return settings;
}
```
Internal topic: t/109980
2023-11-02 13:10:15 +08:00
|
|
|
record.calculate_diff(res[:settings_before], res[:settings_after])
|
DEV: Introduce `run_theme_migration` spec helper in test environment (#26845)
This commit introduces the `run_theme_migration` spec helper to allow
theme developers to write RSpec tests for theme migrations. For example,
this allows the following RSpec test to be written in themes:
```
RSpec.describe "0003-migrate-small-links-setting migration" do
let!(:theme) { upload_theme_component }
it "should set target property to `_blank` if previous target component is not valid or empty" do
theme.theme_settings.create!(
name: "small_links",
theme: theme,
data_type: ThemeSetting.types[:string],
value: "some text, #|some text 2, #, invalid target",
)
run_theme_migration(theme, "0003-migrate-small-links-setting")
expect(theme.settings[:small_links].value).to eq(
[
{ "text" => "some text", "url" => "#", "target" => "_blank" },
{ "text" => "some text 2", "url" => "#", "target" => "_blank" },
],
)
end
end
```
This change is being introduced because we realised that writting just
javascript tests for the migrations is insufficient since javascript
tests do not ensure that the migrated theme settings can actually be
successfully saved into the database. Hence, we are introduce this
helper as a way for theme developers to write "end-to-end" migrations
tests.
2024-05-03 06:29:18 +08:00
|
|
|
|
|
|
|
# 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!
|
FEATURE: Theme settings migrations (#24071)
This commit introduces a new feature that allows theme developers to manage the transformation of theme settings over time. Similar to Rails migrations, the theme settings migration system enables developers to write and execute migrations for theme settings, ensuring a smooth transition when changes are required in the format or structure of setting values.
Example use cases for the theme settings migration system:
1. Renaming a theme setting.
2. Changing the data type of a theme setting (e.g., transforming a string setting containing comma-separated values into a proper list setting).
3. Altering the format of data stored in a theme setting.
All of these use cases and more are now possible while preserving theme setting values for sites that have already modified their theme settings.
Usage:
1. Create a top-level directory called `migrations` in your theme/component, and then within the `migrations` directory create another directory called `settings`.
2. Inside the `migrations/settings` directory, create a JavaScript file using the format `XXXX-some-name.js`, where `XXXX` is a unique 4-digit number, and `some-name` is a descriptor of your choice that describes the migration.
3. Within the JavaScript file, define and export (as the default) a function called `migrate`. This function will receive a `Map` object and must also return a `Map` object (it's acceptable to return the same `Map` object that the function received).
4. The `Map` object received by the `migrate` function will include settings that have been overridden or changed by site administrators. Settings that have never been changed from the default will not be included.
5. The keys and values contained in the `Map` object that the `migrate` function returns will replace all the currently changed settings of the theme.
6. Migrations are executed in numerical order based on the XXXX segment in the migration filenames. For instance, `0001-some-migration.js` will be executed before `0002-another-migration.js`.
Here's a complete example migration script that renames a setting from `setting_with_old_name` to `setting_with_new_name`:
```js
// File name: 0001-rename-setting.js
export default function migrate(settings) {
if (settings.has("setting_with_old_name")) {
settings.set("setting_with_new_name", settings.get("setting_with_old_name"));
}
return settings;
}
```
Internal topic: t/109980
2023-11-02 13:10:15 +08:00
|
|
|
end
|
2024-01-11 14:04:02 +08:00
|
|
|
|
|
|
|
self.reload
|
2024-02-05 14:35:11 +08:00
|
|
|
self.update_javascript_cache!
|
FEATURE: Theme settings migrations (#24071)
This commit introduces a new feature that allows theme developers to manage the transformation of theme settings over time. Similar to Rails migrations, the theme settings migration system enables developers to write and execute migrations for theme settings, ensuring a smooth transition when changes are required in the format or structure of setting values.
Example use cases for the theme settings migration system:
1. Renaming a theme setting.
2. Changing the data type of a theme setting (e.g., transforming a string setting containing comma-separated values into a proper list setting).
3. Altering the format of data stored in a theme setting.
All of these use cases and more are now possible while preserving theme setting values for sites that have already modified their theme settings.
Usage:
1. Create a top-level directory called `migrations` in your theme/component, and then within the `migrations` directory create another directory called `settings`.
2. Inside the `migrations/settings` directory, create a JavaScript file using the format `XXXX-some-name.js`, where `XXXX` is a unique 4-digit number, and `some-name` is a descriptor of your choice that describes the migration.
3. Within the JavaScript file, define and export (as the default) a function called `migrate`. This function will receive a `Map` object and must also return a `Map` object (it's acceptable to return the same `Map` object that the function received).
4. The `Map` object received by the `migrate` function will include settings that have been overridden or changed by site administrators. Settings that have never been changed from the default will not be included.
5. The keys and values contained in the `Map` object that the `migrate` function returns will replace all the currently changed settings of the theme.
6. Migrations are executed in numerical order based on the XXXX segment in the migration filenames. For instance, `0001-some-migration.js` will be executed before `0002-another-migration.js`.
Here's a complete example migration script that renames a setting from `setting_with_old_name` to `setting_with_new_name`:
```js
// File name: 0001-rename-setting.js
export default function migrate(settings) {
if (settings.has("setting_with_old_name")) {
settings.set("setting_with_new_name", settings.get("setting_with_old_name"));
}
return settings;
}
```
Internal topic: t/109980
2023-11-02 13:10:15 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
if start_transaction
|
|
|
|
self.transaction(&block)
|
|
|
|
else
|
|
|
|
block.call
|
2021-03-11 09:15:04 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def convert_list_to_json_schema(setting_row, setting)
|
|
|
|
schema = setting.json_schema
|
|
|
|
return if !schema
|
|
|
|
keys = schema["items"]["properties"].keys
|
|
|
|
return if !keys
|
|
|
|
|
2021-09-27 20:45:05 +08:00
|
|
|
current_values = CSV.parse(setting_row.value, **{ col_sep: "|" }).flatten
|
2023-06-09 04:12:47 +08:00
|
|
|
|
|
|
|
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
|
2021-03-11 09:15:04 +08:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2021-04-29 04:12:08 +08:00
|
|
|
def baked_js_tests_with_digest
|
2022-10-17 22:04:04 +08:00
|
|
|
tests_tree =
|
2024-01-16 09:50:44 +08:00
|
|
|
theme_fields_to_tree(
|
|
|
|
theme_fields.where(target_id: Theme.targets[:tests_js]).order(name: :asc),
|
|
|
|
)
|
2021-04-29 04:12:08 +08:00
|
|
|
|
2022-10-17 22:04:04 +08:00
|
|
|
return nil, nil if tests_tree.blank?
|
|
|
|
|
2024-01-16 09:50:44 +08:00
|
|
|
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)
|
|
|
|
|
2022-10-19 01:20:10 +08:00
|
|
|
compiler.append_raw_script "test_setup.js", <<~JS
|
2021-04-29 04:12:08 +08:00
|
|
|
(function() {
|
|
|
|
require("discourse/lib/theme-settings-store").registerSettings(#{self.id}, #{cached_default_settings.to_json}, { force: true });
|
|
|
|
})();
|
|
|
|
JS
|
2024-01-16 09:50:44 +08:00
|
|
|
|
2022-10-19 01:20:10 +08:00
|
|
|
content = compiler.content
|
|
|
|
|
|
|
|
if compiler.source_map
|
|
|
|
content +=
|
|
|
|
"\n//# sourceMappingURL=data:application/json;base64,#{Base64.strict_encode64(compiler.source_map)}\n"
|
|
|
|
end
|
|
|
|
|
2021-04-29 04:12:08 +08:00
|
|
|
[content, Digest::SHA1.hexdigest(content)]
|
|
|
|
end
|
|
|
|
|
2024-03-21 00:00:43 +08:00
|
|
|
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
|
|
|
|
|
|
|
|
def user_selectable_count
|
|
|
|
UserOption.where(theme_ids: [id]).count
|
|
|
|
end
|
|
|
|
|
2019-07-03 16:18:11 +08:00
|
|
|
private
|
|
|
|
|
2021-11-22 20:16:56 +08:00
|
|
|
attr_accessor :theme_setting_requests_refresh
|
|
|
|
|
2024-01-16 09:50:44 +08:00
|
|
|
def theme_fields_to_tree(theme_fields_scope)
|
|
|
|
theme_fields_scope.reduce({}) do |tree, theme_field|
|
|
|
|
tree[theme_field.file_path] = theme_field.value
|
|
|
|
tree
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-02-03 02:09:41 +08:00
|
|
|
def to_scss_variable(name, value)
|
2022-12-14 03:03:53 +08:00
|
|
|
escaped = SassC::Script::Value::String.quote(value.to_s, sass: true)
|
2021-02-03 02:09:41 +08:00
|
|
|
"$#{name}: unquote(#{escaped});"
|
|
|
|
end
|
|
|
|
|
2019-07-03 16:18:11 +08:00
|
|
|
def find_disable_action_log
|
|
|
|
if component? && !enabled?
|
|
|
|
@disable_log ||=
|
|
|
|
UserHistory
|
|
|
|
.where(context: id.to_s, action: UserHistory.actions[:disable_theme_component])
|
|
|
|
.order("created_at DESC")
|
|
|
|
.first
|
|
|
|
end
|
|
|
|
end
|
2017-04-12 22:52:52 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
# == Schema Information
|
|
|
|
#
|
|
|
|
# Table name: themes
|
|
|
|
#
|
|
|
|
# id :integer not null, primary key
|
2019-01-12 03:29:56 +08:00
|
|
|
# name :string not null
|
2017-04-12 22:52:52 +08:00
|
|
|
# user_id :integer not null
|
|
|
|
# created_at :datetime not null
|
|
|
|
# updated_at :datetime not null
|
|
|
|
# compiler_version :integer default(0), not null
|
|
|
|
# user_selectable :boolean default(FALSE), not null
|
|
|
|
# hidden :boolean default(FALSE), not null
|
|
|
|
# color_scheme_id :integer
|
|
|
|
# remote_theme_id :integer
|
2018-09-20 10:40:51 +08:00
|
|
|
# component :boolean default(FALSE), not null
|
2019-07-03 16:18:11 +08:00
|
|
|
# enabled :boolean default(TRUE), not null
|
2020-11-16 20:44:09 +08:00
|
|
|
# auto_update :boolean default(TRUE), not null
|
2017-04-12 22:52:52 +08:00
|
|
|
#
|
|
|
|
# Indexes
|
|
|
|
#
|
|
|
|
# index_themes_on_remote_theme_id (remote_theme_id) UNIQUE
|
|
|
|
#
|